본문 바로가기
자바

의존성 ( dependency )

by 이상한나라의개발자 2023. 12. 12.

의존관계 (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 의 값에 따라서 주입이 달라집니다.

 

이상으로 의존성에 대해서 알아봤습니다.

 
 
부록
 
의존성 주입과 의존성 역전에 관련해서 test 코드를 예시입니다. 참고 하세요
 
예를 들어, 아래와 같은 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) {
        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