Skip to content

[24기_전유하] spring tutorial 미션 제출합니다. - #8

Open
zzulYH-11 wants to merge 6 commits into
CEOS-Developers:masterfrom
zzulYH-11:master
Open

[24기_전유하] spring tutorial 미션 제출합니다.#8
zzulYH-11 wants to merge 6 commits into
CEOS-Developers:masterfrom
zzulYH-11:master

Conversation

@zzulYH-11

Copy link
Copy Markdown

No description provided.

@whc9999 whc9999 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

스프링이 내부적으로 어떻게 동작하는지 단순 개념 정의보다 한 단계 더 깊게 조사해주신 점이 인상적이었습니다!
특히 Reflection, Bean 생명주기, AOP, PSA까지 서로 연결해서 이해하려고 한 부분이 좋았습니다 👍👍

몇 가지 참고할 수 있는 부분 코멘트 남겼습니다. 각각의 개념이 어떤 역할을 하고, 이들이 어떻게 함께 동작하는지 다시 한번 구분해서 보면 이해하는데 더 좋을 것 같습니당. 첫 과제 하시느라 수고하셨습니다!!!

Comment thread README.md
IoC/DI, AOP, PSA는 스프링을 지탱하는 세 기둥이라고 불리는데, 사실 서로 독립적인 기술은 아니고 아래와 같이 레이어처럼 쌓여 있다.

```
IoC/DI -> AOP -> PSA

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IoC/DI, AOP, PSA를 연결해서 이해하려는 접근은 좋은 것 같습니다! 👍
다만 이 세 개가 계층적으로 쌓이고 PSA가 AOP의 결과물이라고 보는 것은 조금 조심할 필요가 있을 것 같습니당.
특히 @transactional은 PSA와 AOP가 함께 사용되는 대표적인 사례지만, PSA 자체가 AOP를 추상화한 개념은 아닙니다. 각각의 개념을 독립적으로 정리한 뒤 @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.

저도 동감합니다! PSA랑 AOP는 서로 다른 개념이고 Transactional처럼 함께 사용되는 경우가 있다고 표현하면 좋을거 같아요!
이 점을 서현님이 잘 정리해주셨던데, 보시면 좋을거 같아용~

Comment thread README.md

시간 측정과 같이 하나의 기능이 여러 클래스에 흩어져서 반복되는 것들이 있다. 로깅, 트랜잭션 관리, 보안 체크 같은 것들이 그러하다. 이런 것을 Cross-Cutting Concern이라고 하고, 이런 Cross-Cutting Concern들을 하나의 공통 로직으로 처리하도록 모듈화하여 삽입하는 방식이 바로 AOP이다.

스프링은 프록시 패턴을 이용하여 AOP를 지원한다. 이미 DI를 통한 IoC 패턴으로 객체 관리 책임이 프레임워크로 넘어갔으므로, 스프링은 우리가 객체를 호출하면, 스프링은 부가 기능이 포함된 프록시를 만들어 리턴하는 식으로 AOP를 지원하는 것.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AOP 설명에서 "객체를 호출하면 프록시를 만들어 리턴한다"는 표현은 프록시가 호출할 때마다 생성되는 것으로 이해될 수도 있을 것 같습니당
일반적으로 Bean 생성 과정에서 AOP 적용 대상이라면 프록시 객체가 만들어져 컨테이너에서 관리되고, 이후 호출이 해당 프록시를 거친다고 이해하는 게 조금 더 정확할 것 같네용

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

java의 assert는 jvm 실행 옵션에 따라 assertion이 비활성화될 수 있어서 테스트에서 실제 검증이 수행되지 않을 가능성이 있습니다! MockMvc의 content().string(...)이나 AssertJ의 assertThat()을 사용해보면 좋을 것 같습니당 ㅎㅎ

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

덕분에 테스트에서 사용하는 다양한 검증 방법들에 대해 공부해봤습니다. assert는 원래 디버깅 용으로 만들어진 문법이라는 것을 이번에 처음 알았네요.

말씀해주신 대로 content().string(...) 을 사용하도록 코드를 수정해 봤습니다! 직접 body의 값을 꺼낼 필요 없이 메서드 체이닝을 통해 간결하게 검증할 수 있어서 좋은 것 같아요. 좋은 피드백 감사합니다😊

Comment on lines +18 to +20
public List<Test> findAllTests() {
return testService.findAllTests();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

현재는 단순한 실습 Entity라 직접 응답해도 괜찮지만, 실제 서비스에서는 Entity를 Controller에서 그대로 반환하지 않고 Response DTO를 따로 두는 경우가 많습니당
왜 Entity와 API 응답 객체를 분리하는지도 한번 고민해보시면 좋을 것 같습니다!!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

엔티티를 클라이언트에게 응답하면

  • 민감 정보 노출 및 불필요한 데이터 전송
  • LazyInitializationException
  • 양방향 연관관계에서 순환 참조

등의 문제가 생긴다는 것을 공부해봤습니다.

DTO에는 엔티티의 참조 필드를 그대로 두지 않고 응답에 필요한 정보만 필드로 담았기 때문에, LazyInitializationException과 순환 참조 문제를 예방할 수 있었습니다.

Test, TestController, TestService, TestRepository를 test 패키지로 분리하고, test/dto/response 패키지 안에 TestNameResponse DTO를 추가하여 id를 뺀 Name만 응답하도록 코드 수정해봤습니다.

좋은 피드백 감사합니다!

Comment thread README.md

여기서 반복해서 눈에 띈 설계 원칙이 하나 있는데, **역할마다 별도의 객체를 두고 DispatcherServlet은 그 객체들을 "지휘"만 한다**는 점이다. 핸들러를 찾는 일은 `HandlerMapping`에게, 핸들러를 실행하는 방법은 `HandlerAdapter`에게, View를 찾는 일은 `ViewResolver`에게, 예외 처리는 `HandlerExceptionResolver`에게 위임한다. DispatcherServlet 자신은 세부 로직을 갖지 않고 "누구에게 무엇을 물어봐야 하는지"만 알고 각 단계를 순서대로 호출하는 오케스트레이터 역할만 한다. 덕분에 Thymeleaf 대신 다른 템플릿 엔진을 쓰고 싶으면 `ViewResolver` 구현체만 바꾸면 되고, DispatcherServlet 자체는 건드릴 필요가 없다.

마지막으로 이 도구들(`HandlerMapping`, `HandlerAdapter`, `ViewResolver` 등)이 언제 준비되는지도 궁금해서 찾아봤는데, 스프링은 컨테이너가 뜰 때 바로 다 준비해두는 게 아니라 **Lazy-Init 전략**을 쓴다. `DispatcherServlet`이 `init()`을 거치는 시점(첫 요청이 왔을 때, 또는 컨테이너 refresh 직후 `onRefresh()` 콜백)에 `initStrategies()`를 호출해서 필요한 것들을 한꺼번에 초기화하는 것이다. 스프링 애플리케이션의 첫 요청이 유독 느린 이유가 바로 여기 있었다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DispatcherServlet은 HandlerMapping, HandlerAdapter, ViewResolver, HandlerExceptionResolver까지 지휘하는 오케스트레이터 역할만 한다는 말이 가장 객관적으로 다가왔던 것 같아요! 그리고 Lazy Init 전략에 대해서도 이번에 처음 알게 되었는데 왜 유독 첫 실행이 느린지 이제 알게되었습니다! 고생하셨습니다!

@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.

파싱과 리플렉션에 대한 이해 -> BeanDefinition, ASM, AOP 프록시 이해
등으로 이어지는 흐름이 좋아서 스프링 내부 동작에대한 이해를 한 층더 높일 수 있었습니다!
좋은 글 써주셔서 감사해용!

Comment thread README.md
Comment on lines +46 to +77
# IoC/DI

IoC(Inversion of Control)란, 객체의 생성 및 관리 책임이 개발자가 아닌 프레임워크에 있는 디자인 패턴, 혹은 그러한 상태를 말한다.

스프링은 이러한 IoC를 DI(Dependency Injection)라는 것을 통해 구현한다.

- 왜 등장한걸까?

아래 코드를 살펴보자.

```java
public class MemberService {
//private final MemberRepository memberRepositoy = new JpaMemberRepository();
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
```

개발자가 직접 `new()` 를 통해 객체를 생성하고 있다.

이러면 객체 지향 설계 원칙 SOLID의 관점에서 몇가지 결함을 발견할 수 있다.

1. OCP 위반!

OCP는 확장에는 열려있고, 수정에는 닫혀있어야 한다는 것이다. 그러나 만약에 우리가 MemberRepository의 새 구현체를 구현하여 추가하려면, 직접 기존 코드를 수정해야 한다. 즉, 수정에 닫혀있지 못한 코드인 것이다(주석 참고)

2. DIP 위반!

SOLID는 구현체가 아닌 인터페이스에 의존하는 것이 좋은 설계라고 한다.(필수는 아니지만)

위 코드는 new()를 통해 구현체에 의존하고 있으므로, 바람직하지 못하다.

앞서 살펴봤듯, 객체 생성 책임이 개발자에게 있을 때 여러 설계 문제가 발생한다. 스프링에서는 이를 DI로 해결한다고 했는데, 이를 구체적으로 살펴보자.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

OCP와 DIP 위반한 것들을 직접 객체 생성하는 코드와 연결해서 설명해서 잘 와닿았떤 것 같습니당!

Comment thread README.md
- 각 `.class` 파일을 ASM으로 읽어서, `SimpleMetadataReader`가 클래스 이름과 어노테이션 정보만 추출
- `TypeFilter`(기본은 `AnnotationTypeFilter`)로 걸러내서 `@Component`이거나 이를 메타 어노테이션으로 가진 것들만 후보로 채택

이렇게 걸러진 클래스 하나하나가 `ScannedGenericBeanDefinition` 객체로 만들어진다. 이 시점엔 "이런 클래스가 있고, 이런 어노테이션이 붙어있다"는 메타데이터만 존재할 뿐, 실제 인스턴스는 아직 없다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ASM과 리플렉션의 역할을

  1. 메타데이터를 확인하는 과정
  2. 실제 객체를 다루는 과정
    이렇게 나누셔서 설명해주셔서
    두 갈래로 나눠서 이해해볼 수 있었어요!

컴포넌트 스캔에서는 ASM으로 정보를 확인,
실제 객체 생성 단계에서 리플렉션이 사용된다는 걸 이해했습니당

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