Home [soldout] MyBatis 연결을 위한 유연한 구조 변경
Post
Cancel

[soldout] MyBatis 연결을 위한 유연한 구조 변경

# 문제점


DB 연결을 위해 MyBatis를 활용하기로 결정했고 이를 위한 구현을 진행하던 중 Mapper 인터페이스를 주입할 대상에 대한 고민을 하게 되었습니다.

MyBatis의 경우, Service 레이어에 Mapper 인터페이스를 주입시켜 쿼리문을 날려 DB를 사용할 수 있도록 구현이 가능하지만 기존에 의존성 주입이 되어 있든 Repository 인터페이스를 Mapper 인터페이스로 교체해야 하고, 이렇게 된다면 Service 레이어 내에서 호출된 모든 Repository 인터페이스의 메소드도 수정되어야 하는 소요가 발생합니다.

유연한 변경이 가능한 설계를 지향하며 작업을 하고 있던 저에겐 불필요한 레이어의 수정 소요는 OCP에 위배되는 내용이라 생각했고 이러한 문제를 발생시키지 않고 Mybatis를 연동할 수 있는 방법을 고민했습니다.

UserServiceImpl 클래스

1
2
3
4
5
6
7
8
9
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

  // UserMapper로 교체?
  private final UserRepository userRepository;

  // 이하 선언된 메소드도 모두 수정해야 되는 소요가 발생!!
}

# 해결방안


1. Repository와 Mapper 클래스의 역할 구분

블로그 글을 보면 분명하게 Repositry와 Mapper를 다른 Layer로 구분해야 하며 두 개념을 모두 적용해야 한다고 말하고 있습니다.

  • @Repository : DAO의 의미를 가진다.
  • @Mapper : SQL Mapper의 의미를 가진다.

정리하자면 DB에 접근하기 위해 필요한 DTO 객체에 대한 생성 및 검증 절차를 Repository 레이어에서 담당하고, SQL 쿼리문에 대한 책임만을 Mapper 레이어가 가지도록 구성하는 것이 더 올바른 책임의 분산이라고 이해했습니다.

그렇기 때문에 굳이 UserService에서 Mapper 인터페이스를 직접 주입받기 보단 Repository에서 Mapper를 주입받는 새로운 클래스를 만드는 것이 합리적이라 판단했습니다.

2. Composition(조합)

Composition(이하 컴포지션)이란 상속에서 발생하는 여러 문제점을 보안할 수 있는 개념입니다.

상속의 경우 상위 클래스에 선언된 메소드를 하위 클래스에서 그대로 호출해 올 수 있기 때문에 캡슐화가 깨지는 위험이 있습니다. 또한 상위 클래스와 하위 클래스의 결합도가 높아져 상위 클래스의 변경이 하위 클래스에 영향을 줄 수 있다는 단점이 있습니다.

이에 반해 컴포지션은 확장을 위해 생성된 새로운 클래스 내에 기존 클래스 타입의 객체를 주입시켜줌으로써 주입받은 객체의 메소드를 대신 호출해주는 구조로 구성합니다.

이로 인해 객체간의 결합도는 낮아지고 변경사항에 따른 타 클래스의 변경 소요도 없앨 수 있다는 장점이 있습니다.

이러한 컴포지션의 개념을 프로젝트에 적용시켜 본다면 Repository 레이어에서 Mapper 인터페이스 객체를 주입받고 Mapper의 메소드를 대신 호출해주는 구조를 가짐으로써 굳이 다른 상속관계나 인터페이스 구현을 하지 않아도 되는 설계를 가질 수 있습니다.

실제 컴포지션이 적용되는 모습은 아래 어뎁터 패턴에 대한 설명에서 확인할 수 있습니다.

3. Adapter pattern(어뎁터 패턴)

컴포지션 개념을 적용한 디자인 패턴 중 하나를 어뎁터 패턴이라고 볼 수 있습니다. 어뎁터 패턴은 호환되지 않는 형태를 가진 객체를 어뎁터 클래스에서 주입받아 호환 가능한 형태로 메소드를 호출해주는 형태를 가집니다.

위 이미지에서 볼 수 있듯이 클라이언트 객체에서 의존하는 target 인터페이스에선 request() 라는 메소드만 가지고 있습니다. 하지만 클라이언트가 요구하는 사항을 수행해 줄 메소드는 Adaptee 클래스의 specificRequest() 일 경우, 클라이언트 클래스를 수정하지 않고선 의존 객체를 변경할 수 없습니다.

결국 컴포지션 개념을 적용해 Target 인터페이스의 구현체 클래스인 Adapter를 생성하고 Adapter에 Adaptee 객체를 주입시켜 request() 에서 specificRequest()를 대신 호출해준다면 클라이언트 코드를 수정하지 않고 의존성에 대한 변경 없이 확장 및 변경이 가능합니다.

실제 프로젝트에 적용된 어뎁터 패턴의 모습은 다음과 같습니다.

UserServiceImpl 클래스(Client 역할)

1
2
3
4
5
6
7
8
9
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

  // 교체 필요 X
  private final UserRepository userRepository;

  // 이하 생략
}

UserRepository 인터페이스(Target 역할)

1
2
3
4
5
6
7
8
9
public interface UserRepository {

  void save(UserDto user);

  UserDto findByEmail(String email);

  boolean isExistEmail(String email);

}

MyBatisUserRepositoryImpl 클래스(Adapter 역할)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MybatisUserRepository implements UserRepository {
  
  // Adaptee가 되는 객체를 주입
  private final UserMapper userMapper;

  // 오버라이딩된 메소드에선 Adaptee의 메소드를 대신 호출
  @Override
  public void save(UserDto user) {

    userMapper.save(user);

  }
  
  // 이하 생략
  
}

# 마치며


이처럼 Repository와 Mapper의 개념과 책임을 구분하고, 컴포지션을 적용한 어뎁터 패턴을 활용하면서 Service 레이어의 어떠한 수정없이 HashMap을 사용하던 구조에서 Mybatis를 활용해 DB 연동이 가능한 구조로 변경할 수 있었습니다.

# 참고 자료


  • Repo와 Mapper의 차이 : https://umbum.dev/1202?category=1062058
  • 어뎁터 패턴 : https://jusungpark.tistory.com/22
  • 이펙티브 자바 : http://www.yes24.com/Product/Goods/65551284
This post is licensed under CC BY 4.0 by the author.

[soldout] 로그인 검증 기능 구현

[soldout] 로그인 회원 정보 조회를 위한 Resolver 구현