이전 포스팅에서 DB까지 연결한 과정을 정리해봤다.
이번에는 미니 프로젝트를 MVC 패턴에 맞춰 개발하는 과정에서 생긴 궁금증과 그 해답을 정리해보려고 한다.
(지금 생각해보면 어이없는 궁금증도 많았던 것 같다 😂)
Sample Code - UserMapper
@Mapper public interface UserMapper { List<UserProfileDto> getAllUsers(); Integer addUser(UserProfileDto userProfileDto); Integer updateUser(UserProfileDto userProfileDto); UserProfileDto getUserDetail(Long id); Integer deleteUser(UserProfileDto userProfileDto); }
우선 필자가 프로젝트를 진행하면서 작성한 Mapper이다.
다음으로 위 Mapper를 사용하는 UserServiceImpl 코드를 살펴보자
Sample Code - UserServiceImpl
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public List<UserProfileDto> getAllUsers() { return userMapper.getAllUsers(); } }
인터페이스로 정의된 UserMapper의 구현체는 어디에도 없다.
하지만, 위처럼 @Autowired
어노테이션을 사용하여 서비스 레이어에서 Mapper를 의존성 주입을 진행하고 있다.
여기서 필자는 다음과 같은 의문이 들었다.
userMapper는 인터페이스로 정의되어있지만, @Mapper
어노테이션이 붙어있다.
여기서 우리는 MyBatis의 동작 순서를 알아야할 필요가 있다.
MyBatis 동작 순서
- @Mapper 어노테이션이 붙은 인터페이스를 스캔
- XML 파일에서 동일한 namespace를 가진 매퍼를 찾음
- 인터페이스의 메소드와 XML의 SQL id를 매칭
- 동적 프록시를 통해 구현체를 생성하고 스프링 빈으로 등록
따라서 단순히 @Mapper 어노테이션만 붙인다고 해서 의존성 주입이 가능한 것이 아니라, 매퍼 XML 파일에 해당하는 SQL이 정의되어 있어야 하며, 메소드명과 SQL id가 일치해야 한다.
이제 위 조건을 모두 만족하는 XML파일이 존재하여 @Mapper
어노테이션이 붙은 인터페이스와 XML파일이 매핑이 된다면 MyBatis가 내부적으로 프록시 객체를 생성하고 관리하게 된다.
사진은 SQL 쿼리를 작성한 XML인데, namespace로 Mapper를 알려주고 있다.
따라서, 간단하게 정리해보면 다음과 같다.
@Autowired로 사용할 수 있는 이유
1. MyBatis-Spring은 @Mapper 어노테이션이 붙은 인터페이스를 찾는다.
2. 해당 인터페이스에 대한 프록시 구현체를 생성한다.
3. 이 프록시 객체가 스프링 빈으로 등록된다.
4. 따라서 @Autowired로 UserMapper를 주입받으면, 실제로는 MyBatis가 생성한 프록시 객체가 주입된다.
마찬가지로 코드를 작성하면서 비슷한 궁금증이 생겼다.
우선 코드를 먼저 살펴보자
Sample Code
// UserController.java @Controller public class UserController { @Autowired() private UserService userService; @GetMapping("/") public ModelAndView userSelect() { ModelAndView modelAndView = new ModelAndView("/user/userList"); List<UserProfileDto> list = userService.getAllUsers(); return modelAndView.addObject("list", list); } }
// UserService.java public interface UserService { List<UserProfileDto> getAllUsers(); Boolean addUser(UserProfileDto userProfileDto); Boolean updateUser(UserProfileDto userProfileDto); UserProfileDto selectUserDetail(Long userId); Boolean deleteUser(Long userId); }
// UserServiceImpl.java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public List<UserProfileDto> getAllUsers() { return userMapper.getAllUsers(); }
UserService에는 어떤 어노테이션도 붙어있지 않음을 기억하자
여기서 필자는 다음과 같은 궁금증이 생겼다
스프링은 인터페이스 타입으로 의존성 주입을 요청받으면, 해당 인터페이스를 구현한 클래스 중에서 빈으로 등록된 것을 찾아서 주입해준다.
UserServiceImpl에 @Service
어노테이션이 붙어 있기 때문에 구현체인 UserServiceImpl을 찾아서 의존성 주입을 해주는 것이다.
스프링이 @Autowired
를 처리하는 과정을 살펴보자
@Autowired 처리 순서
- UserService 타입의 빈을 찾음
- UserService는 인터페이스이므로, 이를 구현한 클래스들을 검색
- UserServiceImpl에
@Service
어노테이션이 붙어있어 빈으로 등록된 것을 발견- UserServiceImpl의 인스턴스를 userService 변수에 주입
만약 하나의 인터페이스를 구현한 빈이 여러 개라면 @Qualifier
어노테이션을 사용해서 특정 구현체를 지정할 수 있다.
(이전에 의존성 주입 포스팅을 작성했지만 눈여겨보지 못한 부분이었던 것 같다)
코드를 직접 구현하면서 궁금했던 의존성 주입과 관련된 부분을 조금 정리해봤다.
이전에 @Autowired
관련 포스팅을 작성한 적이 있으나, 그 당시에는 미처 생각지도 못한 부분에서 궁금증이 생겼다.
이번 기회에 정리할 수 있어서 다행이라고 생각한다..!
앞으로도 문제가 발생했던 부분을 정리하려고 하는데, 사실 이런 몇가지 부분을 제외하면 패턴의 반복이라 크게 정리할게 없을지도 모르겠다 🥲