Skip to content

[24기_이나경] spring tutorial 미션 제출합니다. - #9

Open
rinarina0429 wants to merge 14 commits into
CEOS-Developers:rinarina0429from
rinarina0429:master
Open

[24기_이나경] spring tutorial 미션 제출합니다.#9
rinarina0429 wants to merge 14 commits into
CEOS-Developers:rinarina0429from
rinarina0429:master

Conversation

@rinarina0429

Copy link
Copy Markdown

No description provided.

- Java 21 및 Jar 패키징 설정
- Spring Web 의존성 추가
- 프로젝트 메타데이터(com.ceos24.springboot) 설정
- build.gradle에 spring-boot-starter-test 의존성 추가
- MockMvc를 활용한 GET "/" 엔드포인트 테스트 작성
- HTTP 200 OK 상태 코드 및 "Hello, Spring Boot" 응답 문자열 검증
- 데이터베이스 연동을 위한 Spring Data JPA 및 MySQL Connector-J 추가
- 보일러플레이트 코드 제거를 위한 Lombok 의존성 추가
- MySQL Datasource 연결 정보 설정
- Hibernate ddl-auto 속성을 create로 설정
- SQL 쿼리 확인을 위한 포맷팅 및 로깅(debug 레벨) 설정 추가

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

IoC/DI, AOP, PSA를 각각 요리사-고기, 은행 시스템, 해외여행 콘센트 비유로 풀어주셔서 개념이 잘 와닿았습니다. 특히 마지막에 세 기술이 어떻게 유기적으로 협력하는지 다이어그램으로 정리해주신 부분이 인상 깊었습니다!!

Comment thread README.md
Comment on lines +41 to +50
@Component
public class Chef {
private final Meat meat;

// 요리사는 추상적인 '고기(Meat 인터페이스)'면 뭐든 받아서 요리합니다.
// 객체 생성과 공급의 제어권(IoC)이 스프링 사장님에게 넘어갔습니다!
@Autowired
public Chef(Meat meat) {
this.meat = meat;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요리사가 직접 돼지등뼈를 사러 가는 코드와, 사장님(스프링 컨테이너)이 고기를 조달해주는 코드를 나란히 비교해주셔서 IoC 개념을 잘 이해할 수 있었습니다!

Comment thread README.md
AOP는 이렇게 가로로 겹치는 횡단 관심사들을 가위로 오려내어 별도의 클래스(Aspect)에 따로 모아둡니다. 그리고 프레임워크에 이 코드들을 런타임 시 앞뒤로 끼워 넣어달라고 설정해둡니다.
결과적으로 개발자는 복잡한 인프라 코드를 신경 쓸 필요 없이, '계좌 이체'라는 순수한 핵심 로직 작성에만 100% 집중할 수 있게 되는거죠.
<br>
만약 AOP가 없다면 개발자는 계좌 이체 코드에도, 대출 승인 코드에도 매번 보안검사(), 트랜잭션시작(), 로깅() 코드를 똑같이 복사해서 붙여넣어야 할겁니다. 보안 검사 방식이 업데이트라도 되면 수백 개의 파일을 열어 일일이 수정해야 하는 끔찍한 일이 벌어지고, 비즈니스 로직은 단 3줄인데, 부가 기능 코드가 20줄을 차지해 코드를 읽기도 힘들어지게 됩니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

횡단 관심사(Cross-cutting Concerns)를 세로/가로로 겹치는 구조로 시각화해주신 설명이 좋았습니다!! 그리고 AOP가 없을 때 발생하는 문제(보안 검사 로직 수백 곳 수정)를 구체적으로 짚어주셔서 "왜 필요한가"가 확실히 와닿았어요

Comment thread README.md
Comment on lines +151 to +159
쉽게 말하면 PSA는 어떤 기술을 사용하든 개발자가 비슷한 방식으로 사용할 수 있도록 중간에 추상화 계층을 만들어주는 것입니다.

예를 들어 우리가 해외여행을 간다고 생각해봅시다.
<br>
한국에서는 콘센트에 충전기를 바로 꽂을 수 있지만, 다른 나라에서는 콘센트 모양이나 전압이 다를 수 있습니다. 그렇다고 여행을 갈 때마다 노트북이나 휴대폰의 충전 방식을 전부 고칠 수는 없겠죠.
그래서 우리는 어댑터를 사용합니다. 사용자는 똑같은 충전기를 사용하고, 어댑터가 각 나라의 콘센트 규격에 맞게 변환해주죠.

Spring의 PSA도 비슷합니다.
개발자가 특정 기술의 세부적인 사용 방법을 일일이 알지 못하더라도, Spring이 제공하는 일관된 인터페이스를 사용하면 내부 구현 기술을 비교적 자유롭게 변경할 수 있도록 만들어줍니다.

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를 해외여행 콘센트 어댑터에 비유하신 부분과 PlatformTransactionManager 인터페이스와 JDBC/JPA/JTA 구현체 관계까지 이어서 설명해주신 흐름을 통해 잘 이해할 수 있었습니다!!

@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주차 고생 많으셨습니다!👊


@Transactional(readOnly = true)
public List<Test> findAllTests() {
return testRepository.findAll();

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를 반환하고, 이를 Controller에서 그대로 반환하고 계시길래 코멘트 남겨요!

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

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도 예방할 수 있고요.

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

.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()은 문자열을 포함해 다양한 타입에 대해 섬세한 검증을 지원하니, 이를 공부해보고 적용해보시면 좋을 것 같아요.

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

꼼꼼히 정리해주셔서 저도 많이 공부가 되었습니다! ㅎㅎ 추천해주신 영상들도 곧 한번 보도록 하겠습니다. 1주차 과제 수고 많으셨어요~~!

Comment thread README.md

---

## 3️⃣ 🔥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.

Servelet 컨테이너의 동작에 대해서 매우 잘 정리해주셨는데요, 그렇다면 Filter와 Interceptor는 Client의 요청 처리 흐름에서 어디에 위치하고 있을까요? 함께 공부해보시면 좋을 것 같습니다.

Comment thread README.md
Comment on lines +234 to +239
먼저 **IoC/DI**를 통해 Spring 컨테이너가 `OrderService`, `OrderRepository`, `TransactionManager` 등의 객체를 생성하고 서로 연결해줍니다.
그리고 `@Transactional`이 붙은 메서드에는 **AOP**가 적용됩니다.
Spring은 실제 `OrderService` 앞에 프록시 객체를 두고 메서드 실행 전 트랜잭션을 시작하고, 정상적으로 끝나면 커밋하고, 문제가 생기면 롤백합니다.
Spring의 선언적 트랜잭션 역시 이러한 프록시와 인터셉터 구조를 사용합니다.
마지막으로 실제 트랜잭션 처리는 PSA를 통해 추상화되어 있습니다.
개발자는 JDBC인지 JPA인지 JTA인지에 따라 서로 다른 코드를 작성하는 대신 Spring의 일관된 트랜잭션 추상화를 사용합니다.

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.

proxy를 활용한 스프링의 AOP에 대해서 매우 잘 이해하고 계신거 같습니다 👍 self-invocation 문제에 대해서도 한번 알아보시면 추후 스프링 이해에 도움이 되실 것 같아요!

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