Notice
Recent Posts
Link
Tags
- 스프링
- sql
- nginx
- spring
- mysql
- 프로그래머스
- Django
- AWS
- hibernate
- java
- PYTHON
- springboot
- string
- @transactional
- 데이터베이스
- 문자열
- SSL
- 스프링부트
- 1차원 배열
- Docker
- 자바
- jpa
- static
- select
- spring security 6
- join
- ORM
- spring boot
- spring mvc
- DI
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
Archives
개발하는 자몽
Java/Spring 테스트 - 2 본문
지난 포스팅에 이어서 작성합니다.
2024.10.02 - [Java] - Java/Spring 테스트 - 1
의존성
의존성
- 컴퓨터 공학에서 말하는 의존성(Dependency)은 결합(Coupling)과 같은 의미로, 다른 객체의 함수를 사용하는 상태를 말함 ⇒ A는 B를 사용하기만 해도 의존한다고 할 수 있음
- 의존성을 약하게 만드는 기술 중 하나가 의존성 주입
- 필요한 값을
new 해서 직접 인스턴스화하는 것이 아닌 외부에서 넣어주는 것new 를 이용하여 인스턴스화 하는 것은 하드 코딩
- 의존성 주입은 의존성을 약화시키는 것, 의존성을 완전히 없애는 방식이 아님
- 의존성 제거 == 객체 간의 협력 부정 / 시스템 간의 협력 부정
- 대부분의 디자인 패턴이나 설계는 어떻게 하면 의존성을 약화시킬 수 있는지 고민한 결과물
- 필요한 값을
의존성 역전
- 의존성 역전(DI) ≠ 의존성 주입(DIP)
- Dependency Inversion ≠ Dependency Injection Principle
- DIP
- 의존성 역전이란?
- 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.
- 다형성의 원리 중 하나
- 모듈 간의 상하 관계를 포함하는 개념
- 화살표의 방향을 바꾸는 기술
- 의존성 역전은 고수준 코드에서 세부 사항에 의존해서는 안됨 <RF. 로버트. C. 마틴 저, 클린 아키텍처>
import 에 사용되는 경로에는 인터페이스나 추상 클래스만을 사용해야 함- 인터페이스나 추상 클래스 같은 추상적인 선언을 정책으로 볼 수 있고, 변동성이 큰 구체적인 요소를 세부 사항으로 볼 수 있음
- 의존성 역전이란?
의존성과 테스트 (의존성과 테스트의 상관관계)
테스트를 잘하려면 다음의 것을 잘 다룰 수 있어야 한다.
- 의존성 주입
- 의존성 역전
예시 | 마지막 로그인 시간
가정 사용자가 로그인할 때마다, 마지막 로그인 시간을 기록해야 함
class User {
private long lastLoginTimestamp;
public void login() {
// ...
this.lastLoginTimestamp = Clock.systemUTC().millis();
}
}
- 의존성이 숨겨져 있는 경우에 해당
- 내부 로직에서
login 은Clock 에 의존적 - 하지만 외부에서
login 함수를 호출할 때는login 함수가Clock 을 사용하는지 모르기 때문
- 내부 로직에서
- 일반적으로 의존성이 숨겨져 있으면 좋지 않은 신호
class UserTest { @Test public void login_테스트() { // given User user = new User(); // when user.login(); // then assertThat(user.getLastLoginTimestamp()).isEqualTo(???); } }
- 마지막으로 로그인한 시간을 비교해야 하는데
??? 에는 어떤 값이 들어가야 하는지 알 수 없음 login 을 호출한 시간과 결과를 비교하는 시점의 시간은 다를 수밖에 없음 → 테스트 방법 X- 시간을 강제로 stub 해 주는 라이브러리가 있지만, 너무 부자연스러움 → 라이브러리가 없으면 테스트가 불가능함
- 이러한 코드를 테스트하는 것은 불가능하고 일관되게 유지하는 것도 어려움
- 마지막으로 로그인한 시간을 비교해야 하는데
예시의 문제점 해결
1. 의존성 주입으로 해결 - 시계를 외부에서 주입받는 경우
class User {
private long lastLoginTimestamp;
public void login(Clock clock) {
// ...
this.lastLoginTimestamp = clock.millis();
}
}
class UserTest {
@Test
public void login_테스트() {
// given
User user = new User();
Clock clock = Clock.fixed(Instant.parse("시간"), ZoneId.of("UTC"));
// when
user.login(clock);
// then
assertThat(user.getLastLoginTimestamp()).isEqualTo(milliseconds로 환산한 시간);
}
}
필요한 값을 만들고, 그 값을
🔹알게 된 점
- 숨겨진 의존성은 테스트하기 힘들게 만든다.
- 의존성은 드러내는 것이 좋다.
🔹문제점
- 외부에서 주입받아 해결했는데, 결국
login 을 사용하는 곳에서도Clock 이라는 숨겨진 의존성을 사용해야 함class UserService { public void login(User user) { // ... user.login(Clock.systemUTC()); // 숨겨진 의존성인 Clock 사용 } }
- 따라서
UserService 테스트가 어려워짐class UserServiceTest { @Test public void login_테스트() { //given User user = new User(); UserService userService = new UserService(); //when userService.login(user); //then assertThat(user.getLastLoginTimestamp()).isEqualTo(???); } }
- 이를 해결하기 위해 또 다른 의존성 주입을 사용하는 것은 해결방법이 아님 →
UserService 를 사용하는 또 다른 코드에Clock 이 숨겨지는 결과. 결국 어딘가에서는 고정된 값을 입력하여 넣어줘야만 함 → 또 테스트하기 힘들어짐 ⇒ 폭탄 돌리기
- 이를 해결하기 위해 또 다른 의존성 주입을 사용하는 것은 해결방법이 아님 →
2. 의존성 역전으로 해결 - 현재 시간을 알려주는 인터페이스 정의
interface ClockHolder {
long getMillis();
}
@Getter
class User {
private long lastLoginTimestamp;
public void login(ClockHolder clockHolder) {
// ...
this.lastLoginTimestamp = clockHolder.getMillis();
}
}
@Service
@RequiredArgsConstructor
class UserService {
private final ClockHolder clockHolder;
public void login(User user) {
// ...
user.login(clockHolder);
}
}
User 는 이제Clock 이 아닌ClockHolder 에 의존ClockHolder 는 외부에서 주입받도록 함UserService 는ClockHolder 를 멤버 변수로 받도록 되어 있음- 유저가 로그인할 때
clockHolder 값을 건네줌
- 유저가 로그인할 때
🔹장점
우선
@Component
class SystemClockHolder implements ClockHolder {
@Override
public long getMillis() {
return Clock.systemUTC().millis();
}
}
- 실제 배포 환경에서 사용할
SystemClockHolder - 현재 시간을 요청하면 시스템 시간을 반환
@AllArgsConstructor
class TestClockHolder implements ClockHolder {
private Clock clock;
@Override
public long getMillis() {
return clock.millis();
}
}
- 위 구현체는 객체를 생성할 때
Clock 을 받도록 함
🔹실제 테스트
class UserServiceTest {
@Test
public void login_테스트() {
// given
Clock clock = Clock.fixed(Instant.parse("시간"), ZoneId.of("UTC"));
User user = new User();
UserService userService = new UserService(new TestClockHolder(clock));
// when
userService.login(user);
// then
assertThat(user.getLastLoginTimestamp()).isEqualTo(milliseconds로 환산한 시간);
}
}
UserService 를 생성할 때TestClockHolder 를 주입User 가 받게 될ClockHolder 는 지정한Clock 만 내려주는ClockHolder 가 될 것
⇒ 쉽게 깨지지 않는 테스트 코드. 항상 같은 결과만 내려주는, 일관된 테스트가 됨
- 배포 환경에서는 어떨까?
SystemClockHolder 는 스프링 빈으로 등록되어 있기 때문에 스프링이UserService 를 만들 때 알아서 잘 주입해 줄 것.
- 결과적으로 의존성 주입과, 의존성 역전 기술 모두 사용함
- 의존성 역전을 사용했을 뿐인데, 배포 환경과 테스트 환경을 분리할 수 있게 됨 ⇒ 추상화 의존 결과
- 의존성 역전
Port-Adapter 패턴 으로도 부름
- 의존성 역전
- 의존성 역전을 사용했을 뿐인데, 배포 환경과 테스트 환경을 분리할 수 있게 됨 ⇒ 추상화 의존 결과
Testability
Testability는 테스트 가능성으로, 얼마나 쉽게 input을 변경할 수 있고, 얼마나 쉽게 output을 검증할 수 있는가를 의미한다.
얼마나 쉽게 Input을 변경...
- Case1.1 - 의존성이 감춰져 있는 경우
class User { private long lastLoginTimestamp; public void login() { // ... this.lastLoginTimestamp = Clock.systemUTC().millis(); } }
- 위처럼
Clock 같은 감춰진 의존성이 존재한다면 input을 외부에서 변경할 수 없음 ⇒ testability가 낮은 코드
- 위처럼
- Case1.2 - Input이 숨겨져 있는 경우
@Getter @Builder(access = 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(); } }
Account 내부에는 인스턴스를 생성하는 팩토리 메서드가 있는데 괜찮을까?
class AccountTest { @Test void create() { //given String username = "foobar"; // when Account account = Account.create(username); // then assertThat(account.getUsername()).isEqualTo("foobar"); } }
username 말고도authToken 이 잘 생성되었는지 확인해야 함authToken 은 UUID로 생성 — "UUID가 사용된다는 이야기는 처음 듣는데요?" → 숨겨진 input: 호출자는 모르는 정보- 호출자가 해당 메서드를 타고 들어와서 내부 알고리즘을 확인함 → 객체지향의 핵심 가치 중 하나인 캡슐화가 깨짐
- 알고리즘을 확인했어도 만들어진
authToken 값이 제대로 된 값인지 확인할 방법이 없음
class AccountTest { @Test void create() { //given String username = "foobar"; String expectedAuthToken = "..."; PowerMockito.mockStatic(UUID.class); when(UUID.randomUUID()).thenReturn(UUID.fromString(expectedAuthToken)); // when Account account = Account.create(username); // then assertThat(account.getUsername()).isEqualTo("foobar"); assertThat(account.getAuthToken()).isEqualTo(expectedAuthToken); } }
- UUID의
randomUUID() 는 static 메서드- static 메서드를 mock 하는 건
mockito 만으로는 안됨 → java의PowerMock 라이브러리를 활용해 봄 - 하지만 찾아보니 UUID는 final class → 어떻게든 방법을 찾으면 해결할 수도 있음 but 이는 테스트가 보내는 신호를 무시하는 것; 잘못된 설계
- static 메서드를 mock 하는 건
- Case 2.1 - 파일 경로 하드 코딩
public class Example { private static final File FILE = new File("data.txt"); public void processData() { //read from FILE and process data } }
- File path가 고정되어 있다면, 고정된 파일에 지나치게 의존하게 돼서 input 변경이 힘듦 ⇒ 낮은 Testability
- Case 2.2 - 외부 시스템 하드 코딩
public class Example { public void processData(String data) { String processData = data.toUpperCase(); // 데이터 처리 sendDataOverNetWork(processedData); } private void sendDataOverNetwork(String data) { // HTTP를 이용한 네트워크 요청 } }
- 스프링을 이용하다 보면
WebClient 나RestTemplate 같은 통신 클래스를 많이 사용하게 됨 → 이를 직접 사용하고 있을 것 → 테스트 어려움
- 스프링을 이용하다 보면
얼마나 쉽게 output을 검증
- Case. 외부에서 결과를 볼 수 없는 경우
public class Example { public void processData(int[] numbers) { int sum = 0; for (int number : numbers) { sum += number; } System.out.println("Sum: " + sum); } }
- 외부에서 콘솔에 출력된 값이 어떤 값인지 확인할 수 없음 → 결과 확인 불가능 ⇒ 낮은 Testability
'Java & Kotlin' 카테고리의 다른 글
[Kotlin] 기본 문법 1 (0) | 2025.01.18 |
---|---|
[Java Error] Caused by: java.lang.IllegalArgumentException at PropertyPlaceholderHelper.java:180 (1) | 2024.11.23 |
Java/Spring 테스트 - 1 (2) | 2024.10.02 |
[Gradle] gradle build와 gradle bootJar의 차이 (0) | 2024.09.06 |
[Java] new Integer와 Integer.valueOf()의 차이점 (0) | 2024.08.11 |