우당탕탕 개발일지

[SpringBoot] 3. 리팩토링 & 스프링 컨테이너 본문

Server

[SpringBoot] 3. 리팩토링 & 스프링 컨테이너

devchop 2025. 1. 5. 16:28

리팩토링하기

이전에 UserController 에서 유저생성, 조회, 수정,삭제 API를 구현하였다. 이것을 리팩토링 해보고, 스프링컨테이너에 대해 알아보자. 컨트롤러에서 하던 작업들은 다음 3개로 분리될것이다.


Controller : API의 진입지점으로써 HTTP Body를 객체로 변환한다
Service : 현재 유저가 있는지등을 확인하여 예외처리 진행
Repository : SQL을 사용해 실제 db와 통신을담당.

 

총 3개의 역할군으로 나누어서 작업을 하도록한다. 그럼 각 코드를 보도록 하자
UserController.java

@RestController
public class UserController {

    private UserService userService;

    public UserController(JdbcTemplate jdbcTemplate){
        userService = new UserService(jdbcTemplate);
    }

    @PostMapping("/user")
    public void saveUser(@RequestBody UserCreateRequest request){
       userService.saveUser(request);
    }

    @GetMapping("/user")
    public List<UserResponse> getUsers(){
      return userService.getUsers(); 
    }

    @PutMapping("/user")
    public void updateUser(@RequestBody UserUpdateRequest request){
        userService.updateUser(request);   
    }

    @DeleteMapping("/user")
    public void deleteUser(@RequestParam String name){
        userService.deleteUser(name);
    }
}

 

UserService.java

//로직을 담당함.
public class UserService {
    private final UserRepository userRepository;

    public UserService(JdbcTemplate _template){
        userRepository = new UserRepository(_template);
    }

    public void saveUser(UserCreateRequest request){
        userRepository.saveUser(request.getName(), request.getAge());
    }
    public List<UserResponse> getUsers()
    {
       return userRepository.getUsers();
    }

    public void updateUser(UserUpdateRequest request){

        if(!userRepository.isUserNotExist(request.getId())){
            throw new IllegalArgumentException("invalid id");
        }

        userRepository.updateUserName(request.getName(), request.getId());
    }
    public void deleteUser(String name){

        if(!userRepository.isUserNotExist(name)) throw new IllegalArgumentException("invalid name");
        userRepository.deleteUser(name);
    }
}

 

UserRepository.java

//실제db와의 통신을 담당함.
public class UserRepository {

    JdbcTemplate jdbcTemplate;
    public UserRepository(JdbcTemplate _template){
        jdbcTemplate =_template;
    }

    public boolean isUserNotExist(int uid){
        String querySQL = "select * from user where id = ?";
       return jdbcTemplate.query(querySQL, (res,rowNum)->0,uid).isEmpty(); 
    }

     public boolean isUserNotExist(String name){
        String querySQL = "select * from user where name = ?";
       return jdbcTemplate.query(querySQL, (res,rowNum)->0,name).isEmpty(); 
    }

    public void saveUser(String name, int age){
         String sql =  "insert info user (name, age) values (?,?)";
        jdbcTemplate.update(sql, name, age);
    }

    public List<UserResponse> getUsers(){
         String sql = "select * from user";

       return jdbcTemplate.query(sql, (rs, rowNum) -> {
        long id = rs.getLong("id");
        String name = rs.getString("name");
        int age = rs.getInt("age");
        return new UserResponse(id,name,age);
        });    
    }
    public void updateUserName(String name, int uid){
        String sql = "update user set name = ? where id= ?";
        jdbcTemplate.update(sql,name, uid);
    }

    public void  deleteUser(String name){
        String sql = "delete from user where name = ?";
        jdbcTemplate.update(sql,name);
    }
}

스프링 컨테이너

userController.java 의 생성자에서 JdbcTemplate을 받아오고 있는데, 이것은 어디에서 자동으로 오는걸까?? 그리고 UserController.javaUserService.java 에서는 이 jdbcTemplate을 사용하고있지 않다. 그렇다면 UserRepository.java 에서 바로 받아올수는 없는걸까?

이는 바로 UserController 위에 있는 @RestController 의 역할이다. 이 어노테이션은 UserController를 스프링 빈으로 설정한다.

 

스프링 빈??
서버가 시작되면, 스프링 서버 내부에 거대한 컨테이너를 만든다. 그리고 이 컨테이너 안에는 여러가지 클래스가 들어간다. 예를들면, 실습에 만든 UserController같은것들. 이때 다양한 정보도 들어있고, 인스턴스화도 진행된다. 즉, 스프링 컨테이너 안에 내가 만든 클래스들이 들어간다. 이때 스프링 컨테이너 안으로 들어간 클래스들을 스프링 빈 이라고 한다.

UserController 에서는 JdbcTemplate이 필요하다. 그런데 스프링 컨테이너 안에는 이미 jdbctemplate이 들어있다. 따라서 인스턴스화 하면서 jdbcTemplate을 넣어주는 것이다. jdbcTemplate은 build.gradle의 dependencies 에 넣어줬기 때문에 스프링 컨테이너에 들어간다.

 

현재 UserRepostory 에서 JdbcTemplate을 가져오지 못하는 이유
jdbtemplate을 바로 가져오려면 UserRepository 가 스프링빈이어야 하는데, 일반 클래스이기 때문. UserRepository 를 스프링빈으로 설정하기 위해서는 클래스이름에 @Repository 어노테이션을 추가한다. UserService를 스프링빈으로 설정하기 위해서는 클래스 이름에 @Service 어노테이션을 추가한다.

UserRepository가 스프링빈이 되면 생성자에서 jdbcTemplate을 받아올 수 있다. userController 와 userService에서는 사용하지 않는 jdbcTemplate을 제거할 수 있게된다.

 

스프링 컨테이너를 사용하는 이유
만약 db가 변경되어, userRepositoryA 를 userRepositoryB 로 변경되었다고 가정해보자.
userService는 userRepositoryA 에 의존되어있었기 때문에, userService의 코드 또한 변경해주어야 한다.

이를 방지하기 위해서는 보통 java의 interface를 사용한다. userService에는 interface IUserRepository 를 사용하고, userRepositoryA와 userRepositoryB는IUserRepository를 상속받아 구현하는 것이다. 이렇게 하면 userService를 수정하지 않아도 된다. 마치 모듈을 갈아끼우는 느낌이다. 하지만 이것도, 수정이 된다면 모듈을 변경해줘야한다. 코드를 수정하는 것은 피할 수 없는것이다.

이를 위해 나온 것이 스프링 컨테이너이다. 컨테이너가 userService를 대신 인스턴스화하고, 그때 알아서 userRepository를 결정해준다. userRepositoryA, userRepositoryB , userService가 모두 스프링빈이라면 , 인스턴스화 과정에서 userService가 어떤 repository를 받을 지 결정하기 때문에 코드를 수정하지 않아도 된다.

@Service
public class UserService{
    IUserRepository repository
    public UserService(IUserRepository repo){
         repository = repo;
    }
}
@Primary
@Repository
public class UserRepositoryA implements IUserRepository{
    @Override
    public void saveUser(){}
}
public class UserRepositoryB implements IUserRepository{
    @Override
    public void saveUser(){}
}

이렇게 제작할 경우, 스프링 빈인 UserRepositoryA 가 자동으로 UserService에 전달된다.
결과적으로 우리는 모듈이 변경되더라도 UserService의 코드를 수정하지 않아도 된다. 둘다 @Repository로 설정되어있을 경우, 추가적으로 @Primary 를 통해 어떤 IUserRepository를 사용할지 결정할 수 있다.

스프링 빈 등록하기

다양한 어노테이션을 이용해 등록가능하다.

 

@Service , @Repository
예시에서 사용된 방법이다. @Service, @Repository 는 개발자가 직접 만든 클래스를 스프링빈으로 등록할 때 사용한다. UserService, UserRepository 가 여기에 해당한다.

 

@Configuration 과 @Bean의 조합

@Configuration 과 @Bean은 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용한다. JdbcTemplate 이 여기에 해당한다.
@Configuration : 클래스에 붙인다. @Bean 을 사용할때 함께 사용해야 한다.
@Bean : 메소드에 붙인다.
예시는 다음과 같다. 보통 configuration 은 config라는 폴더를 만들어 따로 관리한다. userRepository 에 @Repository 가 붙지 않아도 스프링빈으로 등록되고, userService에서 받아올 수 있다.

@Configuration
public class UserConfiguration {

    @Bean
    public UserRepository userRepository(JdbcTemplate jdbcTemplate){
        return new UserRepository(jdbcTemplate);
    }
}

 

@Component
주어진 클래스를 컴포넌트로 간주하고, 스프링 서버 생성 시 자동으로 감지된다. 다른 모든 어노테이션들 모두 @Component 덕분이다. 어노테이션들의 어머니인 느낌이다. 이는 Controller, Service, Repository 모두에 해당되지않는데 스프링빈으로 등록할 필요가 있을 경우 사용한다.

 

스프링 빈 주입받는방법

 

1.생성자를 이용해 주입받는 방식. 현재 사용중인 방식이다.(권장)

2.setter 와 @Autowired 를 사용하는 방법 - 다른 인스턴스로 교체될 가능성이 있음.

public class UserService{
    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
        this.jdbcTemplate = jdbcTemplate;
    }
}

 

3.필드에 직접 @Autowired 를 사용 - 테스트가 어렵다

public class UserService{
 	@Autowired
	 private JdbcTemplate jdbcTemplate;
 }

 

@Qualifier
여러개의 후보군이 있을때 그중 하나를 특정해서 가져오고 싶을 때 사용한다. 
IUserRepository 에 UserRepositoryA, B, C 가 있을때 다음처럼 원하는 서비스를 특정할 수 있다. qualifier은  주입하는쪽과 주입받는쪽 모두에서 사용가능하다. 우선순위는 @Qualifier > @Primary 이다. 


public class UserService{
    IUserService service
    public UserService(@Qualifier("UserRepositoryA") IUserService service){
        this.service = service;
    }
}