묻고 답해요
167만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨장애를 허용하는 견고한 시스템 만들기
[데이터 버저닝을 활용한 멱등성 처리] 멱등성 보장을 위한 version 비교 질문
안녕하세요 준형님! 우선, 강의 정말 잘 들었습니다. 정말 정말 많이 배웠습니다 🙇🏻🙇🏻🙇🏻 다름이 아니라,데이터 버저닝 파트에서 컨슈머의 멱등성을 보장하기 위해주문 서비스가 재고 차감 이벤트에 version을 담고,상품 서비스에서는 JPA가 관리하는 상품 version과 이를 동등성 비교하여중복 소비를 방지하는 방식에 대해 질문드리고 싶습니다. 이 접근 방식에서 많은 인사이트를 얻었지만,상품의 version을 주문 서비스가 반드시 알아야 한다는 점은 조금 와닿지 않았습니다.테스트에서는 version = 0부터 시작하니 문제가 없어 보였으나,여러 이유로 두 version 값이 어긋나면 오히려 소비해야 할 이벤트까지 멱등 처리되어 버릴 수 있다고 생각했습니다.그렇다고 이를 해결하기 위해 주문 서비스가 상품 서비스로부터 version 이벤트를 직접 받아 관리하는 것도 과한 방식처럼 느껴지는데요, 이 부분에 대해 준형님의 의견을 들을 수 있다면 좋을 것 같습니다.시간 편하실 때 부담없이 답변 주시면 감사하겠습니다 🙂
-
미해결실전! 스프링 데이터 JPA
save() vs saveAndFlush DB 통신 횟수
[질문 템플릿]1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예[질문 내용]영한님 안녕하세요.Spring Data JPA가 제공하는 save, saveAndFlush 메서드에 대해 궁금한 점이 있는데요.흔히들 saveAndFlush는 save와 달리 트랜잭션 커밋시점이 아닌, 영속성 컨텍스트 변경 사항을 즉각 DB에 반영하여, DB 통신이 증가한다는 단점이 있다고 합니다.for (int i = 0; i < 3; i++) { repository.save(entity); } 그런데, 제가 의문이 드는 점은 save 메서드도 위와 같은 상황이 있을 때,커밋 시점에 DB에 flush를 하기는 하지만, batch insert가 아니기 때문에 DB 통신 자체는 단 건으로 총 3번 일어나는 것 아닌가요?따라서, DB 통신 자체에서 saveAndFlush가 얻는 이점은 없다고 생각되는 데 어떤 이점이 있을까요? 감사합니다
-
미해결죽음의 Spring Batch: 새벽 3시의 처절한 공포는 이제 끝이다.
kill-9 5TA Alarm
ItemWriter 구현체에 JpaPagingItemReader라고 Write 되어있다. fix 하라
-
미해결실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
fetch join과 영속화와 OSIV의 관계
[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]fetch join의 동작에 대하여 의구심이 들어 찾아봤더니 "fetch join의 경우 SQL에서 사용하는 join의 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능인데요.fetch join은 조회하는 주체가 되는 entity 외에 fetch join이 걸린 연관 관계가 있는 entity까지 함께 select 하여 영속화합니다." 라고 합니다.영속성 컨텍스트는 기본적으로 트랜잭션 범위내에서 생성되고 종료되는거 할거같은데, 어떻게 컨트롤러에서 트랜잭션이 걸려있지 않은 메서드를 바로 호출해서 사용해도 이러한 것이 가능한지 궁금해서 찾아보니 OSIV라는게 있더군요.OSIV는 기본적으로 트랜잭션이 시작 후 종료되어도 일정 부분은 영속성 컨텍스트를 웹요청 전체에 열어둔다.때문에 OSIV를 끄면 지연로딩같은건 컨트롤러에서 이루어지게 코딩해두었으니 예외가 뜬다.근데 여기서 문제는 OSIV를 끄면 fetch join도 작동안해야할거같은데 작동을 합니다. 레포에 눈에 보이지 않는 트랜잭션이라도 걸려있는건가요?
-
미해결실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
자신 필드에도 get으로 접근하는 이유가 있을까요?
[질문 내용]OrderItem에서 가격 총합을 구할 때 자기 자신의 필드임에도 return orderPrice * count;이렇게 바로 쓰기 보다는public int getTotalPrice() { return getOrderPrice() * getCount(); }get으로 가져오시더라구요. 그렇다면 Order에서는//==조회 로직== public int getTotalPrice() { int totalPrice = 0; for (OrderItem orderItem : orderItems) { totalPrice += orderItem.getTotalPrice(); } return totalPrice; }for문에 orderItems에도 getOrderItems()로 하셨었나? 하고 봤더니 이거는 바로 접근을 하시는데자기 필드를 get으로 접근 하는 것이 조금 어색한데, 혹시 이유가 있을까요?
-
미해결토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
UseCase 메서드 단위에 대한 Best Practice
안녕하세요! 토비님.헷갈리는 개념이 하나 있어서 여쭤보고 싶습니다.바로 헥사고날 아키텍처에서 UseCase의 책임 범위인데요.우선 정답은 없다는 것은 알고 있습니다. 다만 Best Practice나 권장되는 방법이 있는지, 그리고 토비님의 고견이 궁금하여 질문드리게 되었습니다. 기능 단위로 UseCase 인터페이스 분리하기 vs 연관된 기능은 UseCase 인터페이스에 묶음으로 제공하기(메서드별로)입니다. 전자는 SRP가 매우 엄격하게 준수되고, 테스트 용이성, 개별 인터페이스별로 정책을 다르게 적용할 수 있다는 장점들이 있지만 과도하게 인터페이스화를 하다 보니 관리할 포인트가 많아져 복잡해진다는 게 단점인 것 같습니다. 후자는 SRP가 엄격하게 준수되지 않더라도, 관련된 기능을 응집도 있게 관리하기 때문에 테스트 용이성이 조금 떨어지고, 일관된 정책을 관리하거나 인터페이스가 비대해질 수도 있다는 단점이 있지만, 응집도 있게 관리하여 유지보수에는 편한 장점이 있는 것 같습니다. 코드를 예시로 보면 아래처럼 콘서트를 조회한다고 했을 때, 일반적으로 PK를 기반으로 조회하지만, 아래와 같이 콘서트명도 unique하고, 가수도 1개의 진행 중인 콘서트만 가지고 있을 수 있을 때 조회 조건이 Id, Name, ArtistName으로 분류될 수 있다고 예시를 들어보겠습니다.public interface GetConcertUseCase { ConcertResult findById(Long concertId); ConcertResult findByName(String name); ConcertResult findByArtistName(String ArtistName); ConcertResult findByIdWithSchedules(Long concertId); // Aggregate Member인 ConcertSchedule 목록 정보도 포함하여 조회 }위에처럼 구성하는 게 후자 방식이고 응집도가 높다고 생각합니다. 그런데 해당 방식은 유스케이스가 비대해질 수 있고, 단일 책임 원칙에서 벗어날 수 있다는 의견 때문에 조회 목적별로 유스케이스 분리하는 것을 권장하는 의견도 있습니다. (전자 방식)public class GetConcertByIdUseCase { ... } public class GetConcertByNameUseCase { ... } public class GetConcertByArtistUseCase { ... } public class GetConcertByIdWithSchedulesUseCase { ... } 정답은 없어서 프로젝트 규모나, 각자의 스타일, 기능 분석에 의해 정해지겠다만, 보편적으로 이런 경우 어떻게 접근하는 게 Best Practice인지 감이 잡히질 않아 질문드리게 되었습니다.
-
해결됨죽음의 Spring Batch: 새벽 3시의 처절한 공포는 이제 끝이다.
스냅샷 읽기
cursorReader에서 스냅샷을 읽어서 동일한 스탭에서 데이터가 변경되어도 영향을 받지 않는다 하셨는데,이거는 mysql의 repeatable read와 관련된 스냅샷인걸까요? 아니면 배치 자체에서 제공하는 스냅샷인가요?
-
미해결실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
Distinct 사용 전 결과에 대한 의문
[질문 템플릿]1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예[질문 내용] { "orderId": 4, "name": "userA", "orderDate": "2025-09-24T02:20:27.580803", "orderStatus": "ORDER", "address": { "city": "서울", "street": "1", "zipcode": "1111" }, "orderItems": [ { "itemName": "JPA1 BOOK", "orderPrice": 10000, "count": 1 }, { "itemName": "JPA2 BOOK", "orderPrice": 20000, "count": 2 } ] }, { "orderId": 4, "name": "userA", "orderDate": "2025-09-24T02:20:27.580803", "orderStatus": "ORDER", "address": { "city": "서울", "street": "1", "zipcode": "1111" }, "orderItems": [ { "itemName": "JPA1 BOOK", "orderPrice": 10000, "count": 1 }, { "itemName": "JPA2 BOOK", "orderPrice": 20000, "count": 2 } ] }Distinct 사용 전 결과는 위와 같이 한 주문에 대해서 두 개의 결과가 나오는건 이해했습니다. 하지만 둘다 OrderItem 배열에 2개의 값이 각각 들어가있습니다.제 생각으로는 한쪽에는 JPA1 BOOK만,다른 한쪽에는JPA2 BOOK만 들어있어야하지 않나? 생각이 드네요. 실제 DB에서의 결과와 동일해야하는거 아닌가요?즉, DB에서의 결과 처럼 아래와 같이 반환되어야 맞지 않나요? { "orderId": 4, "name": "userA", "orderDate": "2025-09-24T02:20:27.580803", "orderStatus": "ORDER", "address": { "city": "서울", "street": "1", "zipcode": "1111" }, "orderItems": [ { "itemName": "JPA1 BOOK", "orderPrice": 10000, "count": 1 } ] }, { "orderId": 4, "name": "userA", "orderDate": "2025-09-24T02:20:27.580803", "orderStatus": "ORDER", "address": { "city": "서울", "street": "1", "zipcode": "1111" }, "orderItems": [ { "itemName": "JPA2 BOOK", "orderPrice": 20000, "count": 2 } ] }
-
미해결실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
레포지토리 계층에서의 트랜잭션에 대한 의문
[질문 템플릿]1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예[질문 내용]본래 DB에 쿼리를 날리는 행위는 트랜잭션 안에서 이루어져야하는데본 강의부터 뒤쪽 강의까지 컨트롤러단에서 직접 레포지토리 메서드를 호출하여 코드를 작성하는걸 볼 수 있습니다.제가 알아본 바로는 아래와 같다고 합니다.쓰기 작업: 반드시 트랜잭션 필요.조회 작업: 트랜잭션 없어도 가능. 단, 지연 로딩 시 트랜잭션 없으면 예외 발생.실무 규칙: Repository는 트랜잭션 안 열고, Service 계층에 @Transactional로 비즈니스 단위 트랜잭션 관리.읽기 전용 조회 최적화: @Transactional(readOnly = true) 사용 → 성능 최적화 + 예외 방지.근데 뒤쪽 강의에서는 fetch join을 사용하니 지연 로딩에 대한 예외가 발생안한다고 쳐도현재 이 강의 초반에 /api/v1/simple-orders 에 대한 메서드에서는 실제로 지연 로딩에 의한 쿼리를 호출합니다.그렇다면 결국 트랜잭션이 없는 상태에서 이루어진 것이니 LazyInitializationException 예외가 발생해야하는거 아닌가요?그리고 보통 실무에서 컨트롤러에서 fetch join 쿼리 메서드(트랜잭션이 걸려있지 않은) 등을 레포지토리에서 바로 호출하는 경우가 흔한가요?
-
미해결죽음의 Spring Batch: 새벽 3시의 처절한 공포는 이제 끝이다.
강의 공부에 대해서 관련된 질문!
공부중인데 내용이 너무 좋아서 블로그에 공부한 예제 코드와 정리된 내용을 일부 작성하려 하는데 괜찮을까요?? 스크린샷으로 첨부하거나 그런건 아니고 코드는 제 ide 에서 실행한것과 git repo 에 올리면서 공유하려고 해~ 그리고 공부한 내용들은 직접 작성 하고 정리해서 올리려고 하는데 괜찮은지 궁금해서 질문해!!
-
미해결토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
repository에 직접 접근
repository에 직접 접근하는 기준이 어떤건지 궁금합니다.강의에서 예를 들면 @Override public Member updateInfo(Long memberId, MemberInfoUpdateRequest memberInfoUpdateRequest) { Member member = memberFinder.find(memberId); checkDuplicateProfile(member, memberInfoUpdateRequest.profileAddress()); member.updateInfo(memberInfoUpdateRequest); return memberRepository.save(member); } private void checkDuplicateProfile(Member member, String profileAddress) { if(profileAddress.isEmpty()) return; Profile currentProfile = member.getDetail().getProfile(); if(currentProfile != null && currentProfile.address().equals(profileAddress)) return; if(memberRepository.findByProfile(new Profile(profileAddress)).isPresent()){ throw new DuplicateProfileException("이미 존재하는 프로필 주소입니다: " + profileAddress); } }이 부분에서 member를 찾아올 때는 memberFinder를 통해서 가져왔는데 아래 중복 체크에서는 repository를 바로 사용했습니다. 물론 강의에서 profile을 찾는거기에 memberFinder에 넣기에는 책임이 난잡해질 것 같다는 느낌은 듭니다만 여기서 구분한 기준이 있는지 궁금합니다.다른 예를 들면 public LoginResponse login(LoginRequest request) { Member member = memberFinder.findByEmail(request.email()); if(!member.verifyPassword(request.password(), passwordEncoder)){ //일치하지 않으면 에러 } //이 부분은 직접 접근하는게 좋은지 member.updateLastLoginAt(); memberRepository.save(member); //아니면 memberUpdater라는 포트를 통해 접근하는게 좋은지 memberUpdater.updateLastLoginAt(); return new LoginResponse(member.getId(), member.getStatus()); }이런 경우 둘중 어떤 방법이 더 구조적으로 좋은지 궁금합니다.
-
미해결실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
24분 27초 연관관계 편의 메서드 위치
[질문 내용]24분 27초에 연관관계 편의 메서드 위치는컨트롤 하는 쪽에 있는 게 좋다. 라고 하셨는데 연관관계의 주인쪽이라고 이해를 했습니다.그렇다면 OrderItem을 추가하는 메서드는Order쪽이 아니라 OrderItem에 해주어야 하는 게 아닐까 생각이 드는데 맞을까요?
-
미해결죽음의 Spring Batch: 새벽 3시의 처절한 공포는 이제 끝이다.
오타 발견 및 건의 및 궁금증
1.오타JobScope와 StepScop가 선언된 빈은 애플리케이션 구동 시점에는 우선 프록시 객체로만 존재한다. 그 후 Job이나 Step이 실행된 후에 프록시 객체에 접근을 시도하면 그 때 실제 빈이 생성된다.StepScop <-오타2.건의(내가틀렸을수도있음 그러면 수정해주셈)그리고 JobParameters가 잡 실행 내부에서 불변이라는 내용이 필요할거같음배치쓰던사람들은 당연한거라 생각할수있는데(잡파라미터랑 잡이름으로 유니크체크한댔나 그런거도 있으니까)배치 첨쓰는사람은 왜 잡파라미터가 있는데 ExecutionContext를 사용하지 라는 생각을 할수있을거같음3.궁금증컴파일 시점에 없는 값을 어떻게 참조할 것인가?여기서 잡의 스텝생성시점에서 di받는 잡파라미터 자리에 null을 넣는식으로 처리하는 방법이 있다고했는데,만약 코틀린의 경우엔 명시적으로 잡파라미터를 ?를 붙여서 nullable로 선언하고 로직에서 NullSafe 박는식으로밖에 해결할수없고 저게 맘에안들면 빈주입해야함?저런 null전달같은 꼼수딴거없음?강의가 자바기반이라고 적혀있어서 물어봐도되나싶긴한데 예전에 저것땜에 고생했었는데 다른방법을 못찾아서 물어봄
-
미해결실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
영속성 컨텍스트 생명주기의 신기한 부분이 있습니다.
1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예[질문 내용]// @Transactional public void update(Long id, String name, String city, String street, String zipcode) { Address address = new Address(city, street, zipcode); Member member = memberRepository.findOne(id); member.setName(name); member.setAddress(address); }위와 같이 update 메서드의 @Transactional 을 주석 처리하고 해봤습니다.제 예상이라면 응답값이 새로워진 값이 아니라 기존의 값이 응답값으로 내려와야 할 것 같았습니다. @PutMapping("/v2/members/{id}") public UpdateMemberResponse updateMemberV2( @PathVariable Long id, @RequestBody @Validated UpdateMemberRequest request ) { memberService.update( id, request.name, request.city, request.street, request.zipcode ); Member findMember = memberService.findOne(id); return new UpdateMemberResponse(findMember.getId(), findMember.getName()); }왜냐하면 결론적으로 엔티티 자체는 수정되었지만 DB에 쿼리가 날라가지 않았고위 컨트롤러 코드에서 find 메서드는 엄연히 다른 트랜잭션에서 진행되니(애초에 업데이트는 트랜잭션이 없었지만) 다른 영속성 컨텍스트가 존재할 것이며,1차 캐시또한 비워져있을 테니(애초에 업데이트는 영속성 객체를 가지고 있지 않았기에 1차캐시에 없음) 즉, 컨트롤러에서 find한 Member 객체는 새로 DB에서 가져온 객체 == 기존 객체 일테니응답 값이 기존 값과 같아야할텐데 아래와 같이 달라진 값을 응답으로 내려주고 있었습니다.왜 그런걸까요?가입 응답{ "name": "hello", "city": "city", "street": "street", "zipcode": "12345" }수정 응답{ "name": "hello@", "city": "city@", "street": "street@", "zipcode": "12345@" }
-
미해결자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]
마이그레이션 오류입니다.
강의에서 강사님께서 11에서 17로 바꾸라는 모든 설정 확실하게 수정 완료했고 gradle 코드 수정하여 실행하니 A problem occurred configuring root project 'library-app'.> java.util.concurrent.ExecutionException: org.gradle.api.GradleException: Failed to create Jar file C:\Users\seong\.gradle\caches\jars-9\3baa90cc2341a9ffd1656b87b2a3526e\spring-core-6.2.11.jar.아래와 같은 jar 파일 오류가 떴습니다. 구글링과 GPT를 써보니 캐시 파일 버전 충돌 오류라고 하여 캐시 파일을 삭제 하려고 해도 디렉터리가 비어있지 않다는 오류가 뜨며 삭제되지 않고, 또 .\gradlew wrapper 버전이 현재 7.5인데 스프링 부트 3.5.6은 최소 8.x를 사용한다고 하여 이것 또한 업그레이드 하려고 하였으나 PS C:\Project\Study-SpringBoot\library-app> .\gradlew wrapper --gradle-version 8.3 --distribution-type allStarting a Gradle Daemon, 5 stopped Daemons could not be reused, use --status for detailsFAILURE: Build failed with an exception.* What went wrong:A problem occurred configuring root project 'library-app'.> java.util.concurrent.ExecutionException: org.gradle.api.GradleException: Failed to create Jar file C:\Users\seong\.gradle\caches\jars-9\3baa90cc2341a9ffd1656b87b2a3526e\spring-core-6.2.11.jar.동일한 이런 jar 파일 오류가 떴습니다.이 오류를 어떻게 해결해야 할까요 .... 도와주세요 ㅜㅜ
-
미해결스프링 시큐리티 OAuth2
JWT 조회 에 대한 질문
안녕하십니까. 스프링 시큐리티 강의를 듣고 있는 대학생입니다. 현재 강의를 들으면서 팀 프로젝트에 소셜 로그인을 적용하던 중 궁금증이 생겨 글을 작성하게 되었습니다. 현재 사용자가 로그인 후 토큰을 발급받고 MySQL에 리프레시 토큰을 저장한 뒤에, 사용자가 API에 접근할 때마다 매번 사용자 정보를 조회하는 것이 좋은 방법인지 궁금합니다. 현재 매번 조회하는 이유는 토큰의 사용자 정보가 서버 데이터베이스에 올바르게 저장된 사용자인지 검증하기 위해서 이 방식을 채택했습니다 package backend_lingua.linguas.infrastructure.security.filter; import backend_lingua.linguas.infrastructure.security.token.enumerated.TokenType; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Slf4j @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getBearerToken(request); // 토큰이 있는 경우에만 검증 if (StringUtils.hasText(token)) { try { if (jwtTokenProvider.validateAccessToken(token)) { Authentication authentication = jwtTokenProvider.getAuthentication(token, TokenType.ACCESS_TOKEN); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("Security Context에 '{}' 인증 정보를 저장했습니다.", authentication.getName()); } } catch (ExpiredJwtException e) { log.error("토큰 인증 실패: {}", e.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed"); return; } } filterChain.doFilter(request, response); } private String getBearerToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } public TokenInfo generateToken(Authentication authentication) { String accessToken = createAccessToken(authentication); String refreshToken = createRefreshToken(authentication); Date accessTokenExpiryDate = createExpiryDate(accessTokenExpiration); return TokenInfo.from(accessToken, refreshToken, accessTokenExpiryDate.getTime(), refreshTokenExpiration); } public String createAccessToken(Authentication authentication) { return createToken(authentication, accessTokenExpiration, accessTokenSecret); } public String createRefreshToken(Authentication authentication) { String token = createToken(authentication, refreshTokenExpiration, refreshTokenSecret); Date expiryDate = createExpiryDate(refreshTokenExpiration); // DB에 리프레시 토큰 저장 tokenService.createRefreshToken(token, expiryDate, authentication); return token; } private String createToken(Authentication authentication, long expirationTime, String secretKey) { String authorities = authentication.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return Jwts.builder() .setSubject(authentication.getName()) // email .claim("auth", authorities) .setIssuedAt(new Date()) .setExpiration(createExpiryDate(expirationTime)) .signWith(createKey(secretKey)) .compact(); } public String getUsernameFromToken(String token, TokenType tokenType) { String secretKey = (tokenType == TokenType.ACCESS_TOKEN) ? accessTokenSecret : refreshTokenSecret; return Jwts.parserBuilder() .setSigningKey(createKey(secretKey)) .build() .parseClaimsJws(token) .getBody() .getSubject(); } public Authentication getAuthentication(String token, TokenType tokenType) { String username = getUsernameFromToken(token, tokenType); UserPrincipal userPrincipal = (UserPrincipal) userDetailsService.loadUserByUsername(username); return new UsernamePasswordAuthenticationToken( userPrincipal, token, userPrincipal.getAuthorities() ); } public boolean validateAccessToken(String token) { return validateToken(token, accessTokenSecret); } public boolean validateRefreshToken(String token) { return validateToken(token, refreshTokenSecret); } private boolean validateToken(String token, String secretKey) { try { Jwts.parserBuilder() .setSigningKey(createKey(secretKey)) .build() .parseClaimsJws(token); return true; } catch (ExpiredJwtException e) { log.error("만료된 JWT 토큰입니다."); throw e; // Filter에서 401 처리하도록 } catch (SecurityException | MalformedJwtException e) { log.error("잘못된 JWT 서명입니다."); } catch (UnsupportedJwtException e) { log.error("지원되지 않는 JWT 토큰입니다."); } catch (IllegalArgumentException e) { log.error("JWT 토큰이 잘못되었습니다."); } return false; } private Date createExpiryDate(long expirationTime) { return new Date(System.currentTimeMillis() + expirationTime); } private SecretKey createKey(String secret) { byte[] keyBytes = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(keyBytes); } @Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final MemberRepository memberRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email)); return UserPrincipal.create(member); } }public class TokenServiceImpl implements TokenService { private final RefreshTokenRepository refreshTokenRepository; private final MemberRepository memberRepository; @Override @Transactional public RefreshToken createRefreshToken(String token, Date expiryDate, Authentication authentication) { String email = authentication.getName(); Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다: " + email)); // 새 토큰 생성 RefreshToken refreshToken = RefreshToken.builder() .token(token) .member(member) .expiryDate(convertToLocalDateTime(expiryDate)) .build(); return refreshTokenRepository.save(refreshToken); } @Override @Transactional public void deleteRefreshToken(String email) { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다: " + email)); deleteRefreshTokenByUser(member); } @Override @Transactional public void deleteRefreshTokenByUser(Member member) { refreshTokenRepository.findByMemberId(member.getId()) .ifPresent(refreshTokenRepository::delete); } private LocalDateTime convertToLocalDateTime(Date date) { return Instant.ofEpochMilli(date.getTime()) .atZone(ZoneId.systemDefault()) .toLocalDateTime(); } }
-
미해결실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
단건 주문만 가능하게 한건 의도한 부분이신가요?
1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예[질문 내용]단순 궁금증입니다.도메인 설계에서 여러 OrderItem을 만들 수 있도록 설계했으면서 정작 Service에선 한 개의 ItemId, count를 받게 하신건 의도하신건가요? 의도하신거였네요.뒷 내용을 확인하지 않고 질문을 달았었네요..
-
미해결스프링 시큐리티 OAuth2
password grant 방식 에러 응답
강의보고 실습하고 있는데요, password grant 타입은 아래와 같이 에러가 발생합니다. 유저 consent도 등록이 되어있는데, 진행이 안됩니다.
-
미해결실습으로 배우는 선착순 이벤트 시스템
흐름정리 제가 이해한게 맞나요?
쿠폰 100개를 발급해야하고 이 개수는 100을 넘어가면 안됨-> java synchronized락의 경우 쿠폰 발급을 담당하는 서버가 여러개라면 db에 쿠폰 저장(개수 업데이트) 요청이 동시에 올수 있으므로 race condition 발생-> redis의 경우 여러 서버가 쿠폰 발급 요청을 보내도 싱글 스레드이므로 쿠폰 발급 수를 정확히 100개로 맞출 수 있음. 하지만 예제의 경우 threadpool을 사용하여 각 스레드별로 쿠폰 발급이 가능하면 바로 db에 쿠폰 저장하는 로직이라서 db 부하가 심함-> kafka를 도입하여 스레드들이 쿠폰 발급 메세지만 카프카로 보내고 카프카 컨슈머가 이 메세지를 처리하여 db에 쿠폰을 저장함. 컨슈머 그룹에 속한 컨슈머는 현재 하나이므로 메세지 1개씩을 처리함 따라서 db에 부하가 심하지 않음.
-
미해결[개정판 2023-11-27] Spring Boot 3.x 를 이용한 RESTful Web Services 개발
JPA
안녕하세요. UserDaoService에 있는 id가1,2,3인 사람들과JPA관련된 data.sql에 있는 id가 90001~90003인 사람들은 따로 관리가 되는 건가요??JPA를 이용하면 사용자들을 DB에 넣어서 관리하는 것이지만 초기에 저희가 아래처럼 추가한 사람들은 관리가 어떻게 되는 것인지 궁금하여 질문드립니다.static { users.add(new User(1,"Kenneth", new Date(), "test1", "111111-1111111")); users.add(new User(2,"Alice", new Date(), "test2", "222222-2222222")); users.add(new User(3,"Elena", new Date(), "test3", "333333-3333333")); }