[LG CNS AM Inspire Camp 1기] SpringBoot (3) - [LAB - 2] MVC를 만들면서

정성엽·2025년 2월 3일
1

LG CNS AM Inspire 1기

목록 보기
37/53

INTRO

이전 포스팅에서 DB까지 연결한 과정을 정리해봤다.

이번에는 미니 프로젝트를 MVC 패턴에 맞춰 개발하는 과정에서 생긴 궁금증과 그 해답을 정리해보려고 한다.

(지금 생각해보면 어이없는 궁금증도 많았던 것 같다 😂)


1. UserMapper는 인터페이스인데?

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가 생성한 프록시 객체가 주입된다.


2. UserService는 인터페이스인데?

마찬가지로 코드를 작성하면서 비슷한 궁금증이 생겼다.

우선 코드를 먼저 살펴보자

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에는 어떤 어노테이션도 붙어있지 않음을 기억하자

여기서 필자는 다음과 같은 궁금증이 생겼다

💡 UserService는 인터페이스인데 어떻게 userServiceImpl을 찾아서 주입해줄 수 있는건가?

스프링은 인터페이스 타입으로 의존성 주입을 요청받으면, 해당 인터페이스를 구현한 클래스 중에서 빈으로 등록된 것을 찾아서 주입해준다.

UserServiceImpl에 @Service 어노테이션이 붙어 있기 때문에 구현체인 UserServiceImpl을 찾아서 의존성 주입을 해주는 것이다.

스프링이 @Autowired를 처리하는 과정을 살펴보자

@Autowired 처리 순서

  • UserService 타입의 빈을 찾음
  • UserService는 인터페이스이므로, 이를 구현한 클래스들을 검색
  • UserServiceImpl에 @Service 어노테이션이 붙어있어 빈으로 등록된 것을 발견
  • UserServiceImpl의 인스턴스를 userService 변수에 주입

만약 하나의 인터페이스를 구현한 빈이 여러 개라면 @Qualifier 어노테이션을 사용해서 특정 구현체를 지정할 수 있다.

(이전에 의존성 주입 포스팅을 작성했지만 눈여겨보지 못한 부분이었던 것 같다)


OUTRO

코드를 직접 구현하면서 궁금했던 의존성 주입과 관련된 부분을 조금 정리해봤다.

이전에 @Autowired 관련 포스팅을 작성한 적이 있으나, 그 당시에는 미처 생각지도 못한 부분에서 궁금증이 생겼다.

이번 기회에 정리할 수 있어서 다행이라고 생각한다..!

앞으로도 문제가 발생했던 부분을 정리하려고 하는데, 사실 이런 몇가지 부분을 제외하면 패턴의 반복이라 크게 정리할게 없을지도 모르겠다 🥲

profile
코린이

0개의 댓글

관련 채용 정보