의존관계 (dependency)
하나의 클래스가 다른 클래스의 기능에 의존할때의 관계를 나타냅니다. 이러한 의존관계는 코드의 유연성, 확정성 및 유지 보수성에 큰 영향을 미칩니다.
1. 의존관계의 표현
- 소스 코드에서 new 키워드로 객체를 생성하는 것 ( 인스턴스 생성 )
- 다른 클래스의 래퍼런스 변수를 사용하는 것
- 다른 클래스를 상속 받는 경우
2. 의존관계의 중요성
- 클래스 간의 강한 의존관계는 변경을 어렵게 만들 수 있습니다. 한 클래스가 변경될 때 연관된 다른 클래스도 변경 되어야 하니까요.
- 테스트에 어려움이 있습니다. 특정 클래스를 독립적으로 테스트 하려 할 때, 해당 클래스가 의존 하는 클래스에도 영향을 미칩니다.
- 강하게 연결된 클래스는 다른 클래스에서 재사용하기 어렵습니다.
아래는 예시 입니다.
위 다이어 그램을 코드로 표현하면 아래와 같습니다.
public class MessageMain {
public static void main(String[] args) {
RealMessageSender sender = new RealMessageSender();
sender.send("Hello, World!");
}
}
public class RealMessageSender {
public void send(String message) {
System.out.println("Real message: " + message);
}
}
public class FakeMessageSender {
public void send(String message) {
System.out.println("Fake message: " + message);
}
}
위 처럼 클래스간의 강한 의존관계는 변경이 어렵게 만들 수 있습니다.
의존성 주입 (dependency Injection)
의존성 주입은 의존관계를 느슨하게 연결하는 방법 중 하나입니다.
직접 new 키워드로 생성하지 않고, 생성자나 setter 메서드를 통해 외부에서 주입 받도록 합니다. 일반적으로는 Spring 프레임워크에서 이러한 의존성 주입을 지원하며 개발자가 더 쉽게 코드의 의존성으로 관리할 수 있도록 도와주죠.
아래는 위 코드의 변경된 다이어그램 입니다.
위 코드에서 변경된 점은 interface를 활용하여 구체 클래스에 대한 의존성을 제거 하였습니다.
이를 통해 다양한 구현체를 쉽게 교체할 수 있으며, 테스트와 확장성 측면에서 용이합니다.
의존성 역전 (dependency Inversion)
먼저 아래 다이어 그램을 확인해보죠
계층을 나누고, 패키지로 구분, 각 계층간 의존성 방향을 제어 하는 전형적인 레이어드 아키텍처 구조입니다.
그리고 UserService 클래스는 UserDataBaseRepository를 의존하고 있습니다.
--> 의존 방향
Controller(저수준) --> Service(고수준) --> UserDataBaseRepository(저수준)
고수준, 저수준의 구분은 Service(유저를 저장한다) -> 무엇을 한다. ( 기술에 종속적이지 않음 )
UserDataBaseRepository ( 유저를 저장하는데 데이터베이스를 사용한다) -> 어떻게 한다 ( 기술에 종속적임 )
고수준의 컴포넌트가 저수준의 컴포넌트에 의존하지 않도록 의존관계를 역전 시키는 것
인터페이스를 통해 의존 방향을 UserRepository에 의존하게끔 하였습니다 ( 의존성 역전 )
의존성을 주입해 주는 주체
그럼 우리는 UserFileRepository 와, UserDataBaseRepository를 어떻게 주입을 받아야 할까요?
그건 스프링 프레임워크가 담당해 주는데요
스프링 프레임워크의 IoC 컨테이너 or Bean Factory ( 애플리케이션 컨텍스트 ) 에서 Bean을 생성하여 Service에 주입을 합니다.
아래 코드를 확인해 보세요.
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/user")
public ResponseEntity<Object> createUser(@RequestBody UserDto userDto) {
userService.createUser(userDto);
return ResponseEntity.created(URI.create("/user")).build();
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
@Transactional
public void createUser(UserDto userDto) {
userRepository.save(userDto);
}
}
public interface UserRepository {
public void save(UserDto userDto);
}
@Component
public class UserDataBaseRepository implements UserRepository {
@Override
public void save(UserDto userDto) {
}
}
UserFileRepository, UserDataBaseRepository 두 클래스에 @Component가 붙어서 이제는 스프링의 Bean 관리 대상이 됩니다.
위 코드는 먼저 UserDataBaseRepository를 주입 받도록 되어있습니다.
우리가 UserFileRepository도 사용해야 하는데요 이럴 경우는 아래와 같이 하셔야 합니다.
@Qualifier
@Service
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(@Qualifier("userFileRepository") UserRepository userRepository) {
this.userRepository = userRepository;
}
...
}
@Component
public class UserFileRepository implements UserRepository {
@Override
public void save(UserDto userDto) {
}
}
@Primary
@Component
@Primary
public class UserFileRepository implements UserRepository {
...
}
@Component
public class UserDataBaseRepository implements UserRepository {
...
}
@Order
@Order(0)
@Primary
public class UserFileRepository implements UserRepository {
...
}
@Order(1)
@Component
public class UserDataBaseRepository implements UserRepository {
...
}
@Profile
@Profile("test")
@Primary
public class UserFileRepository implements UserRepository {
...
}
@Profile("prod")
@Component
public class UserDataBaseRepository implements UserRepository {
...
}
java -jar 로 실행시 active profile 의 값에 따라서 주입이 달라집니다.
이상으로 의존성에 대해서 알아봤습니다.
@Getter
@Builder(access = lombok.AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class Account {
private final String username;
private final String authToken;
public static Account create(String username) {
return Account.builder()
.username(username)
// 우리가 모르는 랜덤 토큰 값이 강제로 세팅 되게 됩니다.
.authToken(UUID.randomUUID().toString())
.build();
}
}
class AccountTest {
@DisplayName("계정을 생성합니다. 의존성 주입을 통해 테스트 가능하게 만듭니다.")
@Test
void accountCreate() {
// given
String username = "test";
// when
// token uuid 값을 테스트 하고자 한다면? 테스트가 불가능 합니다.
Account account = Account.create(username);
// then
assertThat(account.getUsername()).isEqualTo(username);
}
}
위 코드를 보면 계정을 생성할 때 토큰 값이 Account 내부에서 세팅되기 때문에 테스트가 불가능한 상황이 되게 됩니다. 이런 경우 의존성 주입과 역전을 통해서 해결해야 합니다.
@Getter
@Builder(access = lombok.AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class Account {
private final String username;
private final String authToken;
// 토큰 값을 외부에서 주입 받도록 합니다.
// 해당 클라이언트 코드의 변경은 없습니다.
public static Account create(String username, AuthToken authToken) {
return Account.builder()
.username(username)
.authToken(authToken.getAuthToken())
.build();
}
}
// 토큰을 가져오는 인터페이스 추가
public interface AuthToken {
public String getAuthToken();
}
// 테스트 토큰
@AllArgsConstructor
public class TestAuthTokenHolder implements AuthToken {
// 외부에서 토큰 값을 받아서 처리 합니다.
private String authToken;
@Override
public String getAuthToken() {
return authToken;
}
}
// 상용 배포시 사용할 토큰 홀더
public class AuthTokenHolder implements AuthToken {
@Override
public String getAuthToken() {
// 실제 랜덤값을 넣습니다.
return UUID.randomUUID().toString();
}
}
class AccountTest {
@DisplayName("계정을 생성합니다. 의존성 주입을 통해 테스트 가능하게 만듭니다.")
@Test
void accountCreate() {
// given
String username = "test";
String authToken = UUID.randomUUID().toString();
// when
// 테스트 버전
// 의존 주입(DI)과 의존 역전(DIP) 으로 결합도를 낮춰서 테스트 진행
Account account = Account.create(username, new TestAuthTokenHolder(authToken));
// 상용 버전
Account account1 = Account.create(username, new AuthTokenHolder());
// then
assertThat(account.getUsername()).isEqualTo(username);
assertThat(account.getAuthToken()).isEqualTo(authToken);
assertThat(account1.getUsername()).isEqualTo(username);
}
}
'자바' 카테고리의 다른 글
순회 하면서 컬렉션 수정하지 않기 (0) | 2023.12.12 |
---|---|
SOLID (0) | 2023.12.12 |
if문 제거하기 (0) | 2023.12.12 |
Java Optional (0) | 2023.12.12 |
Object Class (1) | 2023.12.11 |