inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

묻고 답해요

173만명의 커뮤니티!! 함께 토론해봐요.

안녕하세요 토비님 개인적인 질문이 있습니다.

해결됨

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

강의와는 관련이 없는데, 개인적인 질문이 있습니다. 저는 자바 스프링 신입 개발자를 준비하고 있는 학생입니다. 지금까지는 MVC 패턴만 사용하고, 모놀리틱 아키텍처를 사용해서 배포를 진행하고 프로젝트를 해왔습니다. 제가 알기로는 학습적으로나 포트폴리오적으로나 필요성을 느껴서 하는 공부가 제일 좋다고 들었습니다. 근데, 최근에는 어떤 필요성을 느끼지 못하면서 대규모 시스템 강의,헥사고날 아키텍처가 중요하다고 하니 강의 등을 듣고 있습니다. 왜냐하면, 본격적인 취업은 내년이고 시간이 좀 남았습니다. 그래서 해당 강의들을 들어두면 언젠가 개인 프로젝트나 현업에서 사용할 수 있지 않을까하고 듣고있습니다만, 제가 사용했던 MVC 패턴의 장단점 등 기본적인 것들도 알지 못하는 상태에서 계속 진도 나가듯이 이런 저런 강의를 듣고 하는게 괜찮을까요? 즉, 아직 기본도 잘 모르면서 계속 새로운 걸 배우는 과정들이 괜찮을까하는 걱정이 드네요. 하지만, 한편으로는 CS지식이 너무 방대해서 기초를 다 잡아두고 다음 단계로 넘어간다는 것도 솔직히 엄두가 안납니다. 그래서 우선은 쭉 이것저것 배워두고 나중에 필요하면 다시 찾아보면서 공부하면 되지 않을까 싶은데, 토비님은 어떤 방향이 더 괜찮다고 생각하시나요??

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
blackhole124 댓글 2 좋아요 1 조회수 314

영상 편집에 오류가 있는것 같습니다.

미해결

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

3:10 MemberRegisterResponse 생성 3:23 MemberRegisterResponse 생성 같은 과정이 반복되는데 편집이 잘 못 된것 같습니다.

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
잉여인간 댓글 1 좋아요 2 조회수 186

MemberInfoUpdateRequest, MemberRegisterRequest의 패키지 위치

미해결

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

학습중에 MemberInfoUpdateRequest, MemberRegisterRequest와 같은 객체들은 어댑터에서도 사용하고, 애플리케이션에서도 사용하고, 도메인 내부로직에도 사용하는데 도메인 패키지 내에 위치하는게 맞는지 의문이 들어서 질문드립니다!

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
처아미나 댓글 2 좋아요 0 조회수 140

MemberRegister가 Member 엔티티의 C/U/D 작업을 모두 담당하나요?

미해결

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

안녕하세요, 토비님. 현재 MemberRegister 인터페이스는 register 뿐만 아니라 활성화/비활성화, 정보 업데이트 등의 오퍼레이션을 제공하고 있는데요, 해당 인터페이스에 작성된 주석은 회원의 등록과 관련된 기능을 제공한다 여서 ' register 를 제외한 오퍼레이션은 다른 인터페이스에 위치해야 하지 않나?' 하는 생각이 듭니다. 혹은 회원 등록 외 오퍼레이션이 해당 인터페이스에 존재하는게 의도하신 바지만, 주석 내용이 수정되지 않은 걸까요? 궁금한 점을 요약해서 정리하자면, xxxFinder 는 조회와 관련된 오퍼레이션을, xxxRegister 는 생성/수정/삭제와 관련된 오퍼레이션을 가지는 걸까요? 완강 후 회사에서 강의로 알려주신 것들을 이것저것 적용해보며 재밌게 일하고 있습니다, 정말 감사드립니다.

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
대흉근 댓글 2 좋아요 0 조회수 203

같은 계층에서의 의존성 관리 질문

해결됨

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

안녕하세요 Toby! 먼저 좋은 강의 올려주셔서 감사 말씀 드립니다. 열심히 강의를 듣던 와중에 궁금한 점이 있어서 질문드립니다. 회원 애플리케이션 기능 추가 강의에서 보면, MemberModifyService 클래스에서 MemberFinder 빈을 주입받아 사용하도록 구현해주셨습니다. 관련해서 같은 application 계층에서 서로 DI 받는 구조로 구성하게 될 경우, 발생하는 순환 참조와 같은 문제점들에 대해서는 어떻게 관리하는게 좋을까요? 그리고 그런 문제점이 발생하지 않도록 예방하기 위해서는 어떤 방법이 있을까요?

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
kaq6822 댓글 2 좋아요 0 조회수 246

프로젝트 오픈 에러

미해결

스프링 DB 2편 - 데이터 접근 활용 기술

제공된 소스 폴더 itemservice-db-start 를 itemservice-db로 변경 후 openProject 하였더니 다음과 같은 빌드 에러가 납니다.. 질문 답변 봐도 별 내용 없는거로 보아 저만 이런거같은데 어떻게 해결하나요

  • spring
  • mvc
  • jpa
  • querydsl
  • spring-data-mybatis
  • spring-jpa
yoon6669 댓글 1 좋아요 0 조회수 175

DB 설정 문제

미해결

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

안녕하세요 프로젝트 초기 설정을 토비님 강의 진행대로 따라하고 있는 중입니다. 제가 docker 는 잘 몰라서 강의 자료에 있는 Rancher Desktop 을 그냥 설치했구요 서버 구동하기 전에 먼저 켜고 토비님 강의 순서 대로 진행했습니다 compose.yaml 의 내용을 수정하기 전에는 오류없이 잘 되는데 토비님이 작성하신 대로 수정을 해서 서버를 구동하면 계속 오류가 발생하고 있습니다 제 디비에 문제가 있나 싶어서 mysql를 완전 삭제하고 재설치 까지 해서 다시 해봐도 이전과 계속 같은 오류가 발생하는데 ai 를 통해서 해결 해보려고 해도 해결을 못하고 있습니다 혹시 확인해보시고 알려주셨으면 합니다

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
댓글 4 좋아요 1 조회수 305

N+1문제

미해결

자바 ORM 표준 JPA 프로그래밍 - 기본편

fetch join을 하는것이 즉시로딩을 하는 개념과 비슷하다고 이해했습니다. 근데 강의에서 지연로딩이든 즉시로딩이든 N+1 문제를 피하기 위해 페치조인을 사용한다고 하셨는데 지연로딩일때는 추가적인 쿼리가 발생하게되어 N+1문제가 생기지만 즉시로딩일때는 모든 데이터를 하나의 쿼리로 가져오기때문에 추가적인 쿼리 없이 N+1문제가 생기지 않는다고 생각했습니다. em.find()등으로 즉시로딩을 할때 N+1문제가 생기지 않는것이고 jpql로 즉시로딩을 하면 N+1문제가 생기는것인가요? 이에따른 이유도 함께 궁금합니다ㅜㅜ

  • java
  • jpa
오리쉐리 댓글 2 좋아요 0 조회수 131

고전파의 테스트 대역 사용 대상, 공유 의존성

해결됨

Practical Testing: 실용적인 테스트 가이드

안녕하세요 복습을 진행하면서 단위 테스트(블라디미르 코리코프)를 같이 공부하는데, gpt와 씨름해 보아도 모르겠어서 질문 드립니다 ㅠㅠ 책에 따르면 고전파의 테스트 대역 사용 대상은 공유 의존성으로 유일하고, 이것의 예로 데이터베이스를 들고 있는데요. 우빈님의 강의에 따르면 이것은 고전파의 방식과는 거리가 멀어 보여서 혼란이 옵니다 테스트 대역을 쓰고 싶다면, 공유 의존성(데이터베이스)은 가능하다라는 뜻 인걸까요? 만약 그렇다면, 고전파가 테스트 대역 사용에 엄격한 방식이라고 이해했었는데, 데이터베이스를 유일한 모킹 가능성 영역이라고 보는 것이 납득하기 어렵습니다 강의에서 가르쳐주신 것처럼 외부 서비스(메일)을 모킹 처리 하는 것이 더 나은 방식, 혹은 고전파 다운 방식이라고 생각되어서 혼란스럽습니다..

  • spring
  • tdd
  • jpa
  • mockito
  • 소프트웨어-테스트
  • junit5
ssshhh 댓글 2 좋아요 0 조회수 194

빌드 문제

미해결

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 1) 여기만 3.3.0(또는 3.2.5 등)으로 변경 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>demo</description> <properties> <java.version>21</java.version> <lombok.version>1.18.36</lombok.version> </properties> <dependencies> <!-- Spring Boot Starters --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- 기타 의존성 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>6.0.3</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> <!-- Jackson --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.14.1</version> </dependency> <!-- Springdoc --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.6.14</version> </dependency> <!-- Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- (선택) JDK 툴체인 강제 설정 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-toolchains-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <goals> <goal>toolchain</goal> </goals> </execution> </executions> <configuration> <toolchains> <jdk> <version>${java.version}</version> </jdk> </toolchains> </configuration> </plugin> <!-- 자바 21 + Lombok 어노테이션 프로세서 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <release>${java.version}</release> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> </annotationProcessorPaths> <fork>true</fork> </configuration> </plugin> <!-- Spring Boot Maven Plugin: Lombok 제외 유지 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
gukin.dev 댓글 1 좋아요 0 조회수 111

시작하려는데 계속 오류가 발생합니다.

미해결

자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "drop table if exists [*]user cascade "; expected "identifier"; SQL statement: 버전도 맞췄는데 뭐가 문제일까요

  • java
  • spring
  • aws
  • mysql
  • spring-boot
  • jpa
u4253 댓글 2 좋아요 0 조회수 94

5.0부터 Querydsl은 향후 fetchCount() , fetchResult() 를 지원하지 않기로 결정했다고 하는데 이에 맞는 강의

미해결

실전! Querydsl

Querydsl fetchResults(), fetchCount() Deprecated(향후 미지원) Querydsl은 향후 fetchCount() , fetchResult() 를 지원하지 않기로 결정했다. 안녕하세요 최근 강의를 구매하고 좋은 강의 잘 들었습니다. 하지만 querydsl은 향후 강의에 나와있는 방식으로는 page처리를 못하게 되는데 이에 맞는 강의도 올라가는 것인가요?

  • java
  • jpa
최어진 댓글 2 좋아요 1 조회수 236

DTO 대신 Form 사용은 안되나요?

미해결

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

[질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예) [질문 내용] 회원 등록 api 에서 아래 코드 처럼 saveMemberV1 메서드의 반환값은 new CreateMemberResponse(id)입니다. @PostMapping("/api/v1/members") public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member){ Long id = memberService.join(member); return new CreateMemberResponse(id); } 근데 MemberForm.java 에 id를 추가하고 이걸로 리턴받으면 안되나요? 왜 굳이 DTO 를 만들어서 리턴하나요?

  • java
  • spring
  • spring-boot
  • jpa
박선정 댓글 1 좋아요 0 조회수 162

[환경설정 PDF 부트 3.0이후 설명 질문] build.gradle에 compileQuerydsl을 정의하지 않은 상태에서 Gradle->Tasks->other->compileQuerydsl을 클릭하라고 하는 이유가 무엇인가요??

미해결

실전! Querydsl

학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요. 1. 강의 내용과 관련된 질문을 남겨주세요. 2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요. (자주 하는 질문 링크: https://bit.ly/3fX6ygx) 3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요. (질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG) 질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요. ========================================= [질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예) [질문 내용] 환경설정 PDF 강의 자료에서는 검증용 Q 타입 생성 Gradle IntelliJ 사용법 Gradle -> Tasks -> build -> clean Gradle -> Tasks -> other -> compileQuerydsl 이라고 되어 있는데 부트 3.0 이후부터는 build.gradle에서 compileQuerydsl 부분이 빠져있기 때문에 'Gradle -> Tasks -> other -> compileQuerydsl' 문구가 다른 문구로 대체되어야 하지 않을까요? 저는 clean 클릭 후 Gradle -> Tasks -> build -> build 를 클릭해서 해결했지만 clean 클릭 후 더 나은 방법이 있지 않을까 싶어서 clean 후 어떤 버튼을 클릭해야 할지 문의드립니다.

  • java
  • jpa
happyrichman 댓글 1 좋아요 1 조회수 239

실무에서 테스트 케이스 작성 시

미해결

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

[질문 템플릿] 1. 강의 내용과 관련된 질문인가요? 예 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예 3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예 [질문 내용] 안녕하세요, 강의 수강 잘 하던 도중 질문이 생겨 여쭤봅니다. 강의에서는 Service 계층에서만 테스트를 하고 있고 Repository 계층에서는 따로 진행하지 않고 있습니다. 실무에서도 어차피 Service 계층이 Repository 계층을 당겨서 사용하는 것이기에 Service 계층에 대해서만 테스트 케이스를 작성하면 충분할까요 ? 그리고 강의에서 Member의 Name을 unique로 잡으라고 말씀하시고 있는데, 실무에서는 id로 검증하는 것이 올바른 방법이겠죠 ?

  • java
  • spring
  • 웹앱
  • spring-boot
  • jpa
wonderwall09in 댓글 1 좋아요 0 조회수 143

로그인 관련 커스텀 훅 만들기 -> 부분 질문입니다.

미해결

코드로 배우는 React 19 with 스프링부트 API서버

현재 loginSlice.tsx에서 오류가 나고 있습니다.. 아래와 같은 메세지를 접했습니다. useCustomLogin.tsx:2 Uncaught SyntaxError: The requested module '/src/store.tsx' does not provide an export named 'AppDispatch' (at useCustomLogin.tsx:2:10) 어느부분을 손 봐야할까요? vite 6.3버전으로 이용하고 있습니다. loginSlice.tsx import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" import { loginPost } from "../api/memberApi" import { removeCookie, setCookie } from "../util/cookieUtil" export interface LoginInfo { email:string, nickname:string, accessToken: string, refreshToken: string, roleNames: string[], status: string } const initState:LoginInfo = { email: '', nickname: '', accessToken:'', refreshToken: '', roleNames: [], status: '' } export const loginPostAsync = createAsyncThunk('loginPostAsync', ({email, pw}: {email:string, pw:string}) => { console.log("---------------loginPostAsync---------------------") console.log(email, pw) return loginPost(email, pw) }) const loginSlice = createSlice({ name: 'loginSlice', initialState: initState, reducers: { save: (state, action) => { console.log("save...........") return action.payload }, logout: (state, action) => { console.log("logout..........") removeCookie("member") } }, extraReducers :(builder) => { builder.addCase(loginPostAsync.fulfilled , (state, action) => { console.log("loginPostAsync.fulfilled") const newState:LoginInfo = action.payload newState.status = 'fulfilled' setCookie("member", JSON.stringify(newState), 1) return newState }) .addCase(loginPostAsync.pending, (state, action) => { console.log("loginPostAsync.pending") state.status = 'pending' }) .addCase(loginPostAsync.rejected, (state, action) => { console.log("loginPostAsync.rejected") state.status = 'rejected' }) } }) export const { save, logout} = loginSlice.actions export default loginSlice.reducer useCustoLogin.tsx import { useDispatch, useSelector } from "react-redux" import { AppDispatch, RootState } from "../store" import { Navigate, useNavigate } from "react-router" import { loginPostAsync, logout, save } from "../slices/loginSlice" import { useEffect } from "react" import { getCookie } from "../util/cookieUtil" const useCustomLogin = () => { const dispatch = useDispatch<AppDispatch>() //로그인 상태 객체 const loginState = useSelector((state: RootState) => state.loginSlice) //로그인 여부 const loginStatus = loginState.status //fulfilled, pending, rejected useEffect(()=> { if(! loginStatus ) { const cookieData = getCookie("member") if(cookieData){ dispatch(save(cookieData)) } } }, []) const navigate = useNavigate() const doLogin = async (email:string, pw:string) => { dispatch(loginPostAsync({ email, pw })) } const doLogout = () => { dispatch(logout(null)) } const moveToLogin = () => { navigate("/member/login") } const moveToLoginReturn = () => { //--------로그인 페이지로 이동 컴포넌트 return <Navigate replace to="/member/login"/> } const moveToPath = (path:string) => { //----------------페이지 이동 navigate({pathname: path}, {replace:true}) } return {loginState, loginStatus, doLogin, doLogout,moveToLogin,moveToLoginReturn,moveToPath} } export default useCustomLogin store.tsx import { configureStore } from "@reduxjs/toolkit"; import loginSlice from "./slices/loginSlice"; const store = configureStore({ reducer: { "loginSlice": loginSlice, } }) export type AppDispatch = typeof store.dispatch export type RootState = ReturnType<typeof store.getState> export default store

  • react
  • typescript
  • spring-boot
  • jpa
  • jwt
별다방 댓글 2 좋아요 0 조회수 82

과제를 위한 초기세팅

미해결

자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

안녕하세요 과제를 하나씩 만들고있는데 4일차 과제인 api만들기를 하려고합니다! 그전까지는 위처럼 기존 강의 초기세팅 된곳에 hw폴더를 만들어서 하고있는데 이렇게 말고 강의초반에 배운대로 spring.io에서 초기세팅을 새로해서 과제를 위한 스프링부트 프로젝트를 새로 만들고싶은데요..! 혹시 현재 강의 초기세팅과 같이 하려면 어떻게해야될지 알려주실 수 있으신가요? 1~2강 초기세팅 강의에서 spring.io로 처음부터 만드는걸 배울때 자바나 스프링부트 버전설정이런건 설명이 있었는데 그다음 의존성 이런건 나중에 설명이 나온다했던거같아서 정확히 모르겠습니다. 강의에 혹시 있다면 어디를 참고하면 될지만이라도 알려주시면 감사드리겠습니다!!

  • java
  • spring
  • aws
  • mysql
  • spring-boot
  • jpa
backendgoat0915 댓글 2 좋아요 1 조회수 81

안녕하세요. 토비님! 도메인의 다양한 업데이트 요구사항을 Web API 계층에서 어떻게 다뤄야 할까요?

미해결

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

안녕하세요, 토비님. 강의를 들으며 많은 인사이트를 얻고 있습니다. 강의를 완강한 후에도 내면적으로 정리되지 않은 부분이 있어 조심스럽게 질문을 드리게 되었습니다. 생성과 관련된 설계는 강의에서 잘 이해가 되었지만, 업데이트(update) 와 관련된 내용은 직접적으로 다뤄지지 않아 고민이 생겼습니다. 특히, 제 고민은 다음과 같습니다. "도메인의 비즈니스 규칙이 Web API 설계에 어느 정도까지 직접적으로 드러나야 하는가?" 현재 도메인 로직에서는 사용자의 여러 정보를 변경할 수 있는 비즈니스 규칙이 존재합니다. 예를 들어: 비밀번호 변경 기타 세부정보 변경 비즈니스적으로는 각각의 규칙이 잘 정의되어 있고, 각각의 변경 로직도 Member 객체 내에 명확히 메서드로 존재합니다. 여기서, 이러한 비즈니스에 대해서 API에 어떻게 노출시켜야 하는가에 대해서 두 가지 선택지가 고려됩니다. 1. 비즈니스 정의를 역할 별로 구성한다. POST /api/v1/members/{id}/change-password POST /api/v1/members/{id}/change-nickname 생각이 나는 장단점은 다음과 같습니다. 장점 : 비즈니스에 따라 API를 관리하여 클라이언트가 이해하기 용이합니다. 단점 : 수정 가능한 필드가 많아질수록 API의 개수가 증가하며, 유지보수가 어려워질 수 있고, Restful 규칙에 위배됩니다. 2. 하나의 update API로 통합한다. PATCH /api/v1/members/{id} { "password": "originalPassword123!", // nullable "detailRequest": { // nullable "email": "user@example.com", "nickname": "nickname123", "password": "newPassword456!" } } 장점 : API가 간결하여 확장이 용이하며, 클라이언트는 필요한 값만 상황에 따라 요청하면 됩니다. 단점 : API가 비즈니스 책임에 명확하지 않을 수 있습니다. 결론적인 질문은 다음과 같이 정리 할 수 있을 것 같습니다. 비즈니스 로직이 도메인 레이어에 잘 분리되어 있는 경우, API 계층에서도 분리하여 표현하는 것이 좋은가요? 도메인의 역할만 명확하다면 API는 통합해서 update 형식으로 만들어도 괜찮은가요? 만약, 후자로 처리를 한다면 어디서 처리를 하는게 좋아보이시나요? 서비스 계층 도메인 계층 // MemberModifyService public void update(Long memberId, MemberUpdateRequest request) { Member member = memberFinder.find(memberId); if (request.password() != null) { member.changePassword(request.password()); } if (request.detailRequest() != null) { member.updateInfo(); } } -------- // MemberModifyService public void update(Long memberId, MemberUpdateRequest request) { Member member = memberFinder.find(memberId); member.update(request); } // Member public void update(MemberUpdateRequest request) { if (request.password() != null) { changePassword(request.password()); } if (request.detailRequest() != null) { updateInfo(); } } 뭔가, 이런 고민이 계속 드는 이유가 외부 계층에 종속적이지 않고 도메인에 의존하여 개발을 하더라도 실제로 저희가 처한 상황은 대부분 WebAPI 계층에서의 요청이 많다보니 외부의 행위 또한 도메인에 종속 되어야 하는가 하는 고민이 생긴 것 같습니다. 양질의 강의 제공해주셔서 감사드립니다!

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
종운 댓글 2 좋아요 0 조회수 267

@NaturalIdCache에 대한 보충 설명 및 사용법 공유

해결됨

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

'25. 엔티티의 자연키 지정' 영상의 후반부에 적용한 @NaturalIdCache 에 대해 추가로 학습한 내용이, 저처럼 해당 애노테이션을 처음 접한 분들에게 도움이 될 것 같아서 글을 작성합니다. 강의에서 오해가 있을 수 있는 부분, 그리고 자연키에 캐시를 적용하는 방법을 정리해 보았습니다. 강의 내용과 실제 동작의 차이점 강의에서는 “같은 트랜잭션 안에서 같은 아이디 값을 가지고 여러 번 조회 시 Persistence Context 에 캐시된 값을 꺼내오는 것 처럼. @NaturalIdCache 를 적용하면 이것도 영속 컨텍스트에 캐싱이 된다.”고 말씀하셨습니다. 해당 내용에 대한 이해를 돕기 위해 Hibernate의 두 가지 캐시에 대해 간단히 짚고 넘어가겠습니다. 1차 캐시 (First-Level Cache): 세션(영속성 컨텍스트) 범위의 캐시입니다. 같은 트랜잭션 안에서만 유효하며, 트랜잭션이 끝나면 사라집니다. Spring Data JPA에서는 기본적으로 @Id 에 대한 조회를 1차 캐시합니다. 2차 캐시 (Second-Level Cache): 세션 팩토리 범위의 캐시로, 여러 세션에서 데이터를 공유할 수 있습니다. 적용하려면 별도의 의존성 추가 및 캐시 관련 설정( @Cache 등)이 필요합니다. 따라서, "같은 트랜잭션 안에서 캐시된 값을 꺼내온다."는 말은 세션 범위의 1차 캐시로 해석됩니다. 하지만 제가 직접 테스트해 본 결과, @NaturalIdCache 는 1차 캐시 가 아닌 2차 캐시 와 관련이 있었으며, 1차 캐시를 적용하기 위해서는 다른 방법이 필요했습니다. 테스트를 통한 확인 자연키에 대한 1차 캐시 동작을 확인하기 위해, 강의에서 적용한 Member 엔티티의 @NaturalIdCache 를 제거 하고, 자연키(Email)에 @NaturalId 만 적용 한 상황에서 두 가지 방식으로 테스트를 진행했습니다. 테스트1: findByEmail 메서드를 사용한 조회 @Test void NaturalIdFirstLevelCache() { Member member = Member.register(createMemberRegisterRequest(), createPasswordEncoder()); memberRepository.save(member); entityManager.flush(); entityManager.clear(); System.out.println("회원 저장 및 persistence context 초기화 완료"); // 같은 email(Natural ID)로 두 번 조회 Member findMember1 = memberRepository.findByEmail(member.getEmail()).get(); Member findMember2 = memberRepository.findByEmail(member.getEmail()).get(); assertThat(findMember1).isSameAs(findMember2); } Spring Data의 쿼리 메서드를 사용하여 이메일로 조회하는 findByEmail 메서드를 만들고, 한 트랜잭션에서 같은 회원을 두 번 조회했습니다. 자연키에 대한 1차 캐시가 동작한다면, SELECT 쿼리는 한 번만 실행되어야 합니다. 결과는 SELECT 쿼리가 두 번 실행 되었습니다. 즉, 자연키에 대한 1차 캐시가 동작하지 않았습니다. 테스트2: Hibernate의 자연키 관련 API를 사용한 조회 @NaturalId 를 다루는 글들을 찾아본 결과 Hibernate가 제공하는 자연키 관련 API가 있다는 것을 확인했고, 이를 적용하기 위해 커스텀 리포지토리를 구현했습니다. @Repository @RequiredArgsConstructor public class CustomizedMemberRepositoryImpl implements CustomizedMemberRepository { private final EntityManager entityManager; @Override public Optional<Member> findByNaturalId(Email naturalId) { return entityManager.unwrap(Session.class) .bySimpleNaturalId(Member.class) .loadOptional(naturalId); } } 그리고, 테스트 1과 같은 방식으로 테스트를 진행하였습니다. @Test void NaturalIdApi() { Member member = Member.register(createMemberRegisterRequest(), createPasswordEncoder()); memberRepository.save(member); entityManager.flush(); entityManager.clear(); System.out.println("회원 저장 및 persistence context 초기화 완료"); Member findMember1 = memberRepository.findByNaturalId(member.getEmail()).get(); Member findMember2 = memberRepository.findByNaturalId(member.getEmail()).get(); assertThat(findMember1).isSameAs(findMember2); } 결과는 SELECT 쿼리가 한 번만 실행 되었습니다. 이를 통해 자연키에 대한 1차 캐시는 @NaturalIdCache 애노테이션과 무관하게, 전용 API를 사용해야만 동작 하는 것을 확인했습니다. @NaturalIdCache의 용도 @NaturalIdCache Javadoc에는 다음과 같은 설명이 있습니다. Specifies that mappings from the natural id values of the annotated entity to the corresponding entity id values should be cached in the shared second-level cache. …중략 This annotation is usually used in combination with Cache , since a round trip may only be avoided if the entity itself is also available in the cache. 대략 “natural id와 상응하는 id에 대한 매핑을 2차 캐시에 저장 하는 애노테이션이고, 엔티티가 캐시되어있어야 하기 때문에 일반적으로 Cache 와 함께 사용 된다.”라고 해석됩니다. 즉, 1차 캐시가 아닌 2차 캐시를 위한 애노테이션입니다. 정리 2차 캐시 관련 설정 및 테스트를 마저 진행한 후 최종 정리한 내용은 다음과 같습니다. 자연키의 1차 캐시 @NaturalIdCache 애노테이션과 관련 없습니다. 자연키에 @NaturalId 만 붙이면 됩니다. 반드시 Hibernate Session의 bySimpleNaturalId() 같은 전용 API를 사용해야 적용 됩니다. 자연키의 2차 캐시 @Cache 와 @NaturalIdCache 를 함께 사용해야 동작합니다. @Cache 만 사용 시 @Id 로 조회할 때만 2차 캐시가 동작합니다. @NaturalIdCache 만 사용 시 자연키와 ID에 대한 매핑 정보는 캐시 히트 되는 걸 확인했지만, ID와 엔티티에 대한 캐시가 없어서 캐시가 적용되지 않았습니다. @Cache 와 @NaturalIdCache 모두 사용 시 ID를 통한 조회와 자연키를 통한 조회 모두 2차 캐시가 적용됩니다. 참고 자료 Hibernate6.6 공식 문서 NaturalCache javadocs baeldung: Hibernate Natural IDs in Spring Boot Spring Custom Repository 글의 오류나 부족한 내용을 알고 계신 분은 코멘트를 달아주시면 감사하겠습니다.

  • java
  • spring
  • spring-boot
  • jpa
  • 리팩터링
  • ddd
spring 댓글 2 좋아요 13 조회수 405

인기 태그

인프런 TOP Writers

주간 인기글