Skip to content

[24기_윤서현] spring tutorial 미션 제출합니다. - #4

Open
alissa159 wants to merge 2 commits into
CEOS-Developers:alissa159from
alissa159:alissa159
Open

[24기_윤서현] spring tutorial 미션 제출합니다.#4
alissa159 wants to merge 2 commits into
CEOS-Developers:alissa159from
alissa159:alissa159

Conversation

@alissa159

Copy link
Copy Markdown

No description provided.

@Hoyoung027 Hoyoung027 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드리뷰를 몇가지 남겼는데 참고해주세요 ㅎㅎ
첫 과제 고생 많으셨습니다!

Comment thread src/main/resources/application.yaml Outdated
datasource:
url: jdbc:mysql://localhost:3306/test_db?allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=UTF-8
username: root
password: root

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

password 등은 추후 과제에서는 숨겨주시면 좋을 것 같습니다~!

Comment thread README.md

---

# 4. Spring MVC

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MVC 패턴, Servlet에 대한 개념이 매우 잘 정리되어있는 것 같습니다 👍 👍
그렇다면 한번 DispatcherServlet의 동작과 연관지어서 Filter와 Interceptor의 동작에 대해 공부해보시기를 추천드려요!

Comment thread README.md
}
}
```
→ 로깅 로직을 한 곳에 모아두고, 원하는 대상(여기선 service 패키지)에 자동으로 적용됨. 비즈니스 로직 코드는 전혀 건드릴 필요 없음

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AOP는 대상 코드를 건드리지 않고 로직을 자동으로 끼워 넣어주는 훌륭한 도구인데요, 그렇다면 실제로 이 코드가 주입되는 시점과 방법은 언제일까요? 아래 키워드를 참고해서 공부해보시면 좋을거 같습니다 ㅎㅎ

  • 프록시 객체
  • JDK Dynamic Proxy / CGLIB

Comment thread README.md
중앙 보안 관제센터(Aspect)를 하나 두고 모든 건물의 출입을 감시하는 것과 비슷하다고 볼 수 있음

### 자주 쓰이는 곳
- @Transactional (트랜잭션 관리)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. @Transactional이 실제로 AOP를 통해 어떤 일을 수행하고 있는걸까요? TransactionAspectSupport.class 내의 invokeWithinTransaction() 메소드를 참고해보시면 좋습니다!
  2. 1번을 공부하신 후에는 트랜잭션에서 self-invocation이 왜 문제가 되는지에 대해 이해해보실 수 있겠네요 ㅎㅎ

@Wannys26 Wannys26 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readme를 실용적인 방향으로 잘 읽어주셔서, 서현님이 써주신 글 바탕으로 다시 잘 공부할 수 있었네요!
왜 필요한지, 코드로 어떻게 사용하는지, 활용은 어떻게 하는지 등
그리고 DI 주입 방법 비교나 여러 구현체를 @qualifier, @primary, List로 주입하는 방법을 비교하여 작성해주신것이 저에게 큰 도움이 되었습니당ㅎㅎ 감사해용~🥹

Comment thread README.md
Comment on lines +116 to +132
예를 들어 @Transactional의 경우:

우리가 코드에서 이렇게만 쓰면:
```java
@Transactional
public void transferMoney() {
// 트랜잭션 처리
}
```
내부적으로 JDBC를 쓰든, JPA를 쓰든, MyBatis를 쓰든 똑같은 코드로 트랜잭션 처리가 가능함
실제 트랜잭션을 처리하는 구현체(PlatformTransactionManager)만 갈아끼우면 되고, 개발자 코드는 안 바뀜
```
// application.yaml 설정에 따라 내부적으로 다른 구현체가 동작
// JPA를 쓰면 → JpaTransactionManager
// JDBC를 쓰면 → DataSourceTransactionManager
// 하지만 개발자가 쓰는 @Transactional 코드는 동일함!
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 트랜잭션을 통해서 AOP와 PSA를 readme에 설명해보았는데요~
동일한 동일한 어노테이션 @transactional을 사용해도 내부에서는 사용하는 기술에 맞는 TransactionManager가 동작한다는 설명이 이해하기 쉬웠습니다!

Comment thread README.md
Comment on lines +308 to +327
스프링 내부에서 일어나는 일을 단순화하면 대략 이런 로직임 (의사코드):
```java
for (Class<?> clazz : scanAllClassesInPackage("com.ceos24.springboot")) {
if (clazz.isAnnotationPresent(Component.class)
|| clazz.isAnnotationPresent(Service.class)
|| clazz.isAnnotationPresent(Repository.class)
|| clazz.isAnnotationPresent(Controller.class)) {

BeanDefinition definition = new BeanDefinition(clazz);
beanFactory.registerBeanDefinition(definition);
}
}

// 이후 등록된 정의를 바탕으로 실제 객체 생성
for (BeanDefinition def : beanFactory.getAllDefinitions()) {
Object bean = createInstance(def); // 생성자 호출
injectDependencies(bean); // @Autowired 필드 채우기
applicationContext.registerSingleton(bean);
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의사코드를 바탕으로 빈 등록 과정을 보여주신게 이해하기 좋았어용.
스프링 동작원리를 알기위해서 내부 프레임 워크 코드까지 읽어보는게 좋을까? 이런 생각을 가지고 있었는데, 서현님 덕분에 공부해볼 용기가 생겼습니다!

Comment thread README.md
Comment on lines +371 to +459
## 인터페이스를 구현한 Service가 여러 개일 때 주입 방법

### 문제 상황
```java
public interface PaymentService {
void pay();
}

@Service
public class KakaoPayService implements PaymentService {
public void pay() { System.out.println("카카오페이 결제"); }
}

@Service
public class TossPayService implements PaymentService {
public void pay() { System.out.println("토스페이 결제"); }
}
```

이 상태에서 그냥 주입받으려고 하면 에러남:
```java
@Autowired
private PaymentService paymentService; // 어떤 걸 주입해야 할지 스프링이 판단 못 함
```
→ `NoUniqueBeanDefinitionException` 발생

### 해결 방법 1: `@Qualifier`로 이름 지정
```java
@Service
@Qualifier("kakao")
public class KakaoPayService implements PaymentService { }

@Service
@Qualifier("toss")
public class TossPayService implements PaymentService { }

// 주입받을 때
@Service
public class OrderService {
private final PaymentService paymentService;

public OrderService(@Qualifier("kakao") PaymentService paymentService) {
this.paymentService = paymentService; // KakaoPayService가 주입됨
}
}
```

### 해결 방법 2: `@Primary`로 기본값 지정
```java
@Service
@Primary // 여러 구현체 중 이걸 기본으로 사용
public class KakaoPayService implements PaymentService { }

@Service
public class TossPayService implements PaymentService { }

// 그냥 주입받으면 자동으로 @Primary가 붙은 KakaoPayService가 주입됨
@Autowired
private PaymentService paymentService;
```

### 해결 방법 3: 전부 다 받아서 List로 관리
```java
@Service
public class PaymentProcessor {
private final List<PaymentService> paymentServices;

// 생성자에 List로 받으면 해당 타입의 모든 Bean이 리스트로 주입됨
public PaymentProcessor(List<PaymentService> paymentServices) {
this.paymentServices = paymentServices;
}

public void payAll() {
paymentServices.forEach(PaymentService::pay);
// 카카오페이 결제
// 토스페이 결제
}
}
```
→ 전략 패턴처럼 여러 구현체를 순회하며 사용하고 싶을 때 유용함

### 언제 뭘 쓸까?
| 방법 | 언제 사용 |
|---|---|
| `@Qualifier` | 특정 상황에서 정확히 어떤 구현체인지 명시하고 싶을 때 |
| `@Primary` | 대부분 이거 쓰고, 가끔 다른 거 쓸 때 |
| `List` 주입 | 모든 구현체를 순회하며 처리해야 할 때 (전략 패턴) |

---

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 service 구현체가 있을때 주입 방법에 대해서
문제 상황 -> 해결 방법을 제시해주신게 좋네용
저도 어떤 문제를 해석해볼 때 이런 방식으로 자주 정리해두곤 합니다. List 주입은 처음 보는거 같은데, 저도 더 공부해보겠습니다!

@zzulYH-11 zzulYH-11 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개념 정리를 핵심만 깔끔하게 잘 해주셨네요! 보면서 도움 많이 됐습니다. 저도 이렇게 글을 정갈하게 쓸 수 있었으면 좋겠네요... 리뷰 간단하게 남겨봤으니 읽어봐주시면 감사하겠습니다. 1주차 과제 수고 많으셨어요~👊👊

Comment thread .gitattributes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오! .gitattributes는 제 프로젝트에 없는 파일이라 공부해보니

  • windows 와 mac/linux의 줄바꿈 문자 차이로 인한 문제 해결하고,
  • jar 같은 바이너리 파일이 텍스트 파일로 잘못 인식되어 줄바꿈 변환 과정에서 손상되는 것 방지하는

역할을 하는 파일이었네요.
원활한 협업을 위해 꼭 신경 써야 할 부분인데 덕분에 하나 배워갑니다. 저도 적용해봐야겠어요👍

.andExpect(status().isOk())
.andExpect(result -> {
String response = result.getResponse().getContentAsString();
assert response.equals("Hello, Spring Boot!");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 마찬가지로 코드 리뷰를 받고 알게 된 내용인데, 도움이 될 수 있을 것 같아 공유드립니다.
맨 밑에 결론을 간단히 요약했으니 그것만 읽어보셔도 좋아요!

이 컨트롤러 단위테스트의 실행 흐름을 보면,

  1. MockMvc를 통해 테스트용 HTTP 요청을 생성해 보내고,
  2. 응답 상태 코드가 200 OK인지 검증한 뒤,
  3. 컨트롤러가 반환한 결과(Body에 담긴 내용)을 문자열로 가져와 출력 내용을 검증

하고 있어요. 여기서 3번의 assert가 문제를 일으킬 여지가 있습니다. Java의 assert 는 JVM 실행시 기본적으로 비활성화되기 때문이죠.

현재 코드에서 검증할 문자열을 다른 값으로 바꾸고 실행해보면, 테스트가 정상적으로 실패하는 걸 확인할 수 있어요. IDE에서 직접 실행하면 JUnit 실행 설정에 보통 -ea 옵션이 자동으로 들어가 있고, IDE가 내부적으로 Gradle을 통해 테스트를 실행하더라도 Gradle의 Test 태스크는 기본적으로 enableAssertions = true를 기본값으로 갖고 있기 때문에 assert가 정상 동작하기 때문이죠.

문제는 이 두 경우를 벗어난 환경이에요. 이 테스트 코드가 Docker 이미지나 GitHub Actions 같은 CI 환경에서 실행될 때, assertion을 활성화하는 옵션이 별도로 설정되어 있지 않다면 JVM은 assert 문 자체를 건너뜁니다. 즉 로컬에서는 정상적으로 실패하던 검증이, CI 환경에서는 조용히 통과해버릴 수 있는 거예요.

그래서 결론은!

”Java의 assert 키워드보다는 MockMvccontent() 메서드나 AssertJ의 assertThat() 등을 사용하여 테스트를 검증하자”입니다.

원래 assert는 디버깅용으로 만들어진 문법이기도 하고, assertThat()은 문자열을 포함해 다양한 타입에 대해 섬세한 검증을 지원하니, 이를 공부해보고 적용해보시면 좋을 것 같아요.


@GetMapping
public List<Test> findAllTests() {
return testService.findAllTests();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller에서 Entity(Test)를 그대로 반환하고 계시길래 코멘트 남겨요!

이미 아는 내용일 거라는 생각이 들지만, 제가 이번에 코드 리뷰를 받고 공부한 내용이기도 하고, 이번 과제에 리뷰할 코드 양 자체가 적기도 하여… 남겨봅니다. 편한 마음으로 읽어보시면 좋을 것 같아요

결론부터 말씀드리면, Entity를 그대로 클라이언트에게 응답하기보다는 Response DTO를 따로 만들어서 반환하는 게 좋을 것 같아요.

Entity를 직접 반환하면 이런 문제들이 생겨요.

  • 클라이언트가 필요 없는 데이터까지 네트워크로 전송됨. 만약 사용자의 주소, 비밀번호 같은 민감한 정보가 포함되어 있다면 더 큰 문제
  • 향후 응답 구조를 수정해야 할 때 Entity와 API 응답 구조가 강하게 결합되어 있어 변경에 유연하게 대응하기 어려움
  • 참조 관계가 있는 Entity를 응답에 쓰면 LazyInitializationException이 발생할 수 있고, 양방향 참조라면 순환 참조 문제도 같이 발생할 수 있음

순환 참조 문제를 조금만 더 자세히 알아볼게요.

아래처럼 User와 Post가 아래처럼 양방향 연관관계를 맺고 있는 상태에서,

@Entity
public class User {
@OneToMany(mappedBy = "user")
private List<Post> posts; //리스트가 아닌 단일 객체여도 마찬가지로 문제가 발생해요.
}

@Entity
public class Post {
@ManyToOne
private User user;
}

User를 그대로 응답에 반환한다고 가정해봅시다.

그러면 Jackson 라이브러리가 이를 JSON으로 직렬화할 때 User → posts → Post → user → User → ...를 계속 반복하다가 StackOverflowError가 발생할 수 있어요.

@JsonIgnore@JsonManagedReference/@JsonBackReference 를 통해 이를 막을 수도 있긴 하지만, 이건 Entity에 직렬화용 어노테이션을 또 붙이는 거라 JPA 매핑 책임이랑 섞여버려서 그다지 깔끔한 해결책은 아닌 것 같아요.

그래서 응답용 DTO를 생성하여 이를 응답에 사용하는 것을 추천드려요.

Service 계층에서 필요한 필드만 담아 DTO를 조립한 뒤 Controller로 넘기면, 애초에 응답 DTO에는 역참조 필드가 없도록 설계할 수 있으니 순환 참조 자체를 구조적으로 예방할 수 있어요.

또한 @Transactional이 적용된 Service 계층에서 필요한 Lazy 연관관계에 접근해 DTO 조립까지 끝낸다면, Controller에서 지연 로딩 필드를 건드리다 발생하는 LazyInitializationException도 예방할 수 있고요.

덤으로 같은 도메인이어도 목록용/상세용처럼 여러 형태의 응답 객체를 자유롭게 만들 수 있다는 것도 장점입니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants