묻고 답해요
161만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
상품상세-코드 / ProductSection의 분리 이유
안녕하세요, 강사님. Product의 상세 정보(Full Information)를 ProductSection으로 분리한 이유가 궁금합니다. 섹션3.상품상세 - 코드 느끼기 수업 중 07:50 지점에서'상품 상세 정보(Full Information 부분)를 이미지만이 아닌, HTML과 이미지의 복합으로도 구성할 수 있다고 전제하고 ProductSection을 만들었다.'고 하셨는데 '상품 상세 정보를 이미지+HTML로 구성'하는 것이 ProductSection으로 분리'한다는 결정에 어떤 영향을 준건지 모르겠습니다. 제가 생각해 본 경우는 다음과 같습니다.상품의 Full Information이 여러 이미지와 HTML 요소로 구성되니 하나의 상품에 여러 요소가 연관되는 일대다 관계를 만들기 위해 별도의 개념으로 분리시켰다.Full Information을 이미지 또는 HTML로 구성할 때 이미지인지, HTML인지 타입 정보(ProductSectionType)가 필요하니 높은 응집성을 위해 별도의 개념으로 분리시켰다. 상품 정보에 특정 속성에 대한 정보(이미지인가, HTML인가)가 속성으로 포함되는 것보다는 아예 분리하는 것이 응집성이 높지 않을까 싶습니다.위의 부분 외에 분리의 이유를 예상해보면 다음고 같은 듯 합니다.ProductSection이 상품의 본질은 아니고 '상품을 어떻게 보여줄 것인가, 어떻게 판매할까'에 관한 것상품의 생성과 함께 반드시 따라 붙어야하는 종류는 아님조회 패턴, 수정 패턴이 다름상품 정보보다는 상품 섹션이 더 자주 바뀔 것 같습니다.수업 잘 듣고있습니다.감사합니다.
-
미해결자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)
강사님
백엔드 취준생이 고 강의랑 관련 된 내용은 아니지만강사님 강의를 2개 정도 구매했습니다. 지금 자바로 프로젝트 2개를 한 상태입니다.로드 맵 이 있어서 듣게 됐습니다.보통 Error 관련 코드를 만들 때 @ExceptionHandler 로 컨트롤러에서 발생한 에러를 잡고 @ControllerAdvice 가 모든 에러를 잡아서 관리 해 주는 걸로 다른 강의에서 배웠는데 보통 현업에서는 이렇게 하나요? 제가 프로젝트 두 개 모두이런식으로 enum 과 class 를 따로 만들어서 했는데 현업에서 어떻게 하는 지 궁금해서 질문드립니다. @Getter public class BusinessException extends RuntimeException { private final HttpStatus status; private final ErrorCode errorCode; public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.status = errorCode.getErrorCode(); this.errorCode = errorCode; } public BusinessException(HttpStatus status, ErrorCode errorCode, String message, Throwable cause) { super(message, cause); this.status = status; this.errorCode = errorCode; } public BusinessException(HttpStatus status, String message) { super(message); this.status = status; this.errorCode = null; } } @Getter @AllArgsConstructor public enum ErrorCode { TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "토큰이 없습니다."), JWT_EXPIRED(HttpStatus.BAD_REQUEST, "jwt 토큰이 만료되었습니다. "), INVALID_JWT(HttpStatus.BAD_REQUEST, "jwt 토큰을 찾을 수 없습니다."), ACCEPTED_EXISTS(HttpStatus.CONFLICT, "팔로우를 찾을수없습니다."), FOLLOW_NOT_FOUND(HttpStatus.CONFLICT, "팔로우를 찾을수없습니다."), INVALID_FOLLOW_STATUS(HttpStatus.CONFLICT, "팔로우상태가 아닙니다."), AGREEMENT_INPUT(HttpStatus.CONFLICT, "약관 동의가 필요합니다."), INVALID_EMAIL_INPUT(HttpStatus.BAD_REQUEST, "해당 이메일은 소셜 로그인 계정입니다. 소셜 로그인을 이용하세요."), DUPLICATE_RESOURCE(HttpStatus.FORBIDD
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
현재 아키텍처 구조에서의 트랜잭션 관련해서 질문이 있습니다.
안녕하세요 강의 잘 보고 있습니다! 현재 아키텍처는Controller -> Service -> Resposiotry 구조로 흐름이 이어지고 Service는 각 개념에 대한(Product, Review 등..) 조회 쿼리를 묶는 Finder, 그외 CUD 기능을 묶는 Mananger 또는 Handler ? 로 이뤄져 있습니다.이 구조는 예전에 토스 슬래시 "지속 성장 가능한 코드를 만들어가능 방법"에 대해 인상 깊게 봤는데 조회용, CUD용을 별도 컴포넌트로 관리하고 Service는 이를 핸들링 하는 파사드 패턴의 구조와 유사하게 생각했었습니다. 여기서 질문이 있는데요Product와 Review는 격벽을 두는 서로 다는 개념이지만 Product라는 1급 개념이외의 하위 개념 (섹션, 카테고리)들은 상품을 추가할 때 하나의 트랜잭션으로 묶이는 대상이라고 생각이 들었습니다. 그럼 이 경우에는 리뷰는 상품과 서로 다른 개념이기 때문에 서로를 알지 못하지만, Product의 하위 급수 개념들은 ProductService 내에서 하나의 트랜잭션으로 묶는,즉, xxxService는 동일한 개념에 속한 개념들을 하나의 트랜잭션으로 묶는 파사드의 일종이라고 생각하면 될까요? 만약 맞다면 개념과 격벽을 이해하기에 각 개념은 하나의 트랜잭션으로 묶여야하나?에 대한 고민도 개념을 구분하는 방법이 될 수 있겠구나!라는 생각이 들기도 하네요 항상 잘 보고 있습니다 감사합니다!
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
상품목록/코드느끼기 - 룰셋에 의한 암묵적 구현과 운영진과의 소통
안녕하세요, 강사님.강의 잘 듣고 있습니다. '섹션2. 상품 목록 - 코드 느끼기' 수업에서룰셋에 의한 암묵적인 구현으로 '카테고리-상품 맵핑이 존재하는 상품은 ACTIVE 상태'임을 보장할 때, 다음 두 가지 방식을 말씀하셨습니다.연관된 맵핑이 존재한다면 상품을 삭제할 수 없다.상품을 삭제할 때 연관된 맵핑이 함께 삭제된다.어떤 방법을 선택할지는 운영진들과 소통해야한다고 하셨는데 (16:30~)처음에는 운영진과 왜 소통이 필요한건지 잘 이해가 안되어서 고민해보니맵핑이 단순히 다대다 연관관계 표현을 넘어 도메인 개념으로 동작하여 운영진에게 노출되는 경우를 말씀하신건가 싶습니다. 만약 그렇지 않다면 설명 부탁드립니다. 예시 맵핑에 추가적인 정보가 붙는다고 가정해보면 연관관계 표현뿐 아니라 하나의 엔티티, 도메인 개념으로 기능하고 운영진들에게도 노출된다.ex) 프로모션 이벤트에 포함된 상품마다 할인율이 다르다면 promotion-product 맵핑에 할인율이 포함되고, 운영진들도 프로모션-상품 조합마다 할인율을 조정해야함. ->promotion-product 맵핑 자체가 업무 개념이 되어 운영진에게 노출되고 사용되어짐.룰셋에 의한 암묵적 구현은 다음 방식들 중 선택할 수 있다.모든 프로모션에서 A 상품을 제외해야 A 상품을 삭제할 수 있다. (하나라도 맵핑이 있다면 상품 삭제 불가)A 상품을 삭제하면 모든 프로모션에서 제외된다. (상품 삭제 시 연관된 모든 맵핑 제거)따라서 어떤 룰셋을 정의할지 운영진들과의 소통이 필요하다.
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
격벽에 대한 내용을 코드로 표현 시 질문이 있습니다!
안녕하세요 유투브를 쭉 챙겨보다 강의를 내셔서 바로 달려온 구독자 중 한명입니다 !다름이 아니라 섹션 2의 개념 정리에서 개념도의 격벽에 대한 내용을 코드로는 어떤식으로 표현되는지를 좀더 확실히 이해하고 싶어 질문드립니다. 현재 상품과 카테고리 테이블 구조는 N : N으로 되어있고 ProductCategory는 이 다대다 관계를 연결하는 매핑 테이블로 이해했습니다. 그렇다면 상품과 카테고리는 서로가 서로를 알지 못하는 격벽으로 나눠져있으며 모든건 이 매핑 테이블인 ProductCategory 로 이뤄지고상품과 카테고리의 정보를 가져오기 위해서는 두 개념은 서로를 알지 못하니 ProductFinder 에서ProductCategoryRepository 를 사용해 Category를 얻은 후 Product를 반환한다.가 제가 이해한 격벽을 구현한 코드가 맞을까요 ? 또 이 격벽을 위한 코드에서 조인을 사용하지 않는 이유가 궁금했습니다.RDBMS과 NoSQL의 차이점 하나가 JOIN으로 알고 있는데 JOIN을 사용하지 않고 개별적으로 접근한다는 점이 조금 새롭게 다가왔거든요.기존에 다른 수강생 분들의 QA를 종합해서 정리했을 때는 다음과 같이 정리됐습니다.무적의 한방 쿼리 사용을 지양 (쿼리가 점점더 비대해짐) 조인을 사용하지 않음으로써 다양한 조합(재사용)이 가능복잡한 쿼리문을 들여다보지 않고 각 쿼리문들을 통해 로직의 흐름을 파악할 수 있다???제가 이해한 내용이 맞을까요?? 항상 잘 보고있습니다! 감사합니다!
-
미해결프로덕션 레벨 실시간 채팅 서버 구축: 분산 처리부터 성능 최적화까지 (Kotlin & Spring)
빌드 파일
빌드 파일을 파일 마다 각각 만드는데 그렇게 하는 이유는 무엇인가요???
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
SettlementTargetSummary가 db core 모듈에 있는 이유가 있을까요?
제가 느끼기엔 SettlementTargetSummary는 개념 객체에 좀 더 가까운 것 같은데, 혹시 db core 모듈에 위치해 있는 특별한 이유가 있을까요? 코드 자체도 @Entity 어노테이션이 붙지 않은 걸 보니 테이블로 관리하지 않는 것 같은데 여기에 두신 이유가 궁금합니다! 감사합니다.
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
데이터 검증 로직 책임에 대한 질문이 있습니다.
안녕하세요. 정말 재밌게 수강중입니다. 다름이 아니라 데이터 검증 책임에 대해서 궁금한게 있습니다.저는 사용자에 의해 넘겨받은 데이터를 Controller 에서는 값의 존재여부와 타입 정도만 확인하고 실제로 비즈니스 레이어에서 사용될 때 검증하는 것을 선호하는데, 강의에서는 Request(DTO) 객체에서 toContent() 함수를 호출하면서 검증하더라구요.특별히 Request 에 위치시킨 이유가 있을까요?관점이 궁금합니다. fun toContent(): QuestionContent { if (title.isEmpty()) throw CoreException(ErrorType.INVALID_REQUEST) if (title.length > 100) throw CoreException(ErrorType.INVALID_REQUEST) if (content.isEmpty()) throw CoreException(ErrorType.INVALID_REQUEST) return QuestionContent(title = title, content = content,) } 해당 로직이 아래 방식으로 들어가는게 책임이 맞지 않을까? 라는 생각입니다. data class QuestionContent( val title: String, val content: String, ) { init { if (title.isEmpty()) throw CoreException(ErrorType.INVALID_REQUEST) if (title.length > 100) throw CoreException(ErrorType.INVALID_REQUEST) if (content.isEmpty()) throw CoreException(ErrorType.INVALID_REQUEST) } }
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
조회 시 개념간 격벽
안녕하세요.상품 상세조회 섹션을 보고 궁금한 점이 생겨 질문을 드립니다 !평소에는 개념간 격벽을 신경쓰지만 조회 시에는 지키기가 쉽지 않더라고요.특히 list 조회인데 다른 개념이 있어서 조회 + in query + 조립 등을 하면 너무 과한가..? 싶은 생각이 가끔 듭니다.제가 생각하는 방법과 트레이드 오프입니다.개념간 격벽을 유지하고, 쿼리를 분리아무래도 복잡해지는 구현DB 커넥션을 위한 오버헤드 증가팀원의 공감을 생각보다 얻기 힘듦(사실 저부터 확신이 없는...)조회의 경우 느슨한 규정화면 요구사항에 따라 변하는 쿼리재사용성 x주체가 모호함점점 쌓이는 비슷한 쿼리와 projection dto들거의 혼자 백엔드 개발을 진행하다보니 이런 고민에 대해 선택을 내리기 참 어렵네요 ㅎㅎ..제미니님은 어떤식으로 선택하시는지 궁금합니다 !감사합니다.
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
cancel관련질문
안녕하세요.강의내용중 payment는 자기가 취소되어있는지 모르지만 대신 cancel은 payment를 사용한다고 말씀하시는데이게 어떤의미인지 잘 이해가 되지 않습니다.상태반영은 order쪽에 하는데 cancel이 payment를 사용한다?? 이게 어떤의미일까요??
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
따닥방지
안녕하세요. 저였으면 찜추가 찜삭제 API 를 나누는걸로도 방지할 수 있다고 생각해요.말씀해주신것처럼 컨트롤러에서 분기문 들어가는게 좀 어색한것같아서요! 이런 방식도 가능할까요? 뭔가 저는 Restful 관점에서도 이게 자연스러우니 이렇게 해결하실 것 같았는데 언급이 없으셔서 문제가 있을수도있나 싶어서 여쭈어봅니다!
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
enum 에 도메인 로직이 들어가도 될까요?
안녕하세요!현업에서 도메인(core) 모듈에 enum 들도 같이 두고, 타입 판단 로직 등 도메인 비즈니스 로직을 enum 클래스 내부에도 응집시켜 놓는 편인데요~ (bo 객체 클래스가 따로 존재하지 않거나 객체 클래스 보다는 enum 클래스에서 판단하는 것이 역할이 더 맞다고 생각되는 경우 ex enum 생성 static 메서드 등) 현재 예시 프로젝트의 구조에서는 core-enum이 core-api 의 도메인 로직 등을 의존하지 않고 완전 독립적으로 존재하기 때문에 enum class에는 비즈니스 로직이 존재하면 안된다는 것을 의도하신 것 같아 질문드립니다! enum 은 로직에서 아예 제외시키는 편이신가요? 그렇다면 이유가 궁금합니다.
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
getOwnedCoupons 의 null 처리
coupon 데이터 처리시 !! 연산자 처리하는 부분에 질문이 있습니다. 논리적으로 현재 기획상으로는 null 일리 없음. -> !! 연산자를 사용이 상황으로 코드를 이해하긴 했어요. 하지만, 이러한 방식은 일종의 암묵지라서 버그가 발생할 여지가 있어보이는데요. 특히 쿠폰처럼 사람이 개입하는 경우에 어드민에서 상태를 변경하는 경우 기존의 전제가 성립하지 않을 때가 있잖아요. (쿠폰을 비활성화 한다거나)이런 경우 몇가지 선택지가 있을거 같아요. 1. 쿠폰은 임의로 비활성화하지(or 다른 상태도 불변) 않는다. 2. 쿠폰이 비활성화 되는 경우, 소유 쿠폰에서 제거 된다.이런 경우,, 기획에 따라 다르다라고 이해해야할까요? 저 같은 경우, 이미 발급된 소유 쿠폰 자체를 불변처리 하는쪽으로 얘기하는 편이 심플해보이긴 합니다만, 재민님 의견도 궁금합니다.
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
찜목록 조회시 product 의 상태
찜 목록을 조회해야하는 상황에서, Product 의 상태가 전이되는 경우가 있을거 같은데요. 예를들어, 어떤 이유로 soft delete 되거나, 판매자가 숨김 처리를 하거나,, 이런 경우에는 물론,, 회사마다 정책이 다른거 같긴하더라구요. 예를들어, 들어가니 404 페이지가 뜨는 경우도 있고, 안보여주는 경우도 있구요. 이때 고민되는 부분이 찜 목록 (페이지네이션 한다는 가정) 을 조회할 때 아래의 문제들이 발생하는거 같은데, 혹시 어떻게 푸는게 좋을까요? 1. product 의 상태를 이벤트로 받아서, 찜 목록을 처리한다. -> 이 경우 찜이 많이 된 경우 (유명한 아이템이라 100만개의 찜이 있는) 처리가 애매해보이더라구요. 2. join 을 통해 풀어준다. -> 현재는 상품의 찜 목록이라, 사실 같은 팀내에 같은 서비스가 접근 가능해서 join 이 가능할거 같은데, 이게 찜이 아니라, 나의 리뷰보기 같은 경우 다른 팀에 있을 가능성이 있어서 join 을 통해 풀기 어려운 경우도 있을거 같다는 생각도 드네요.
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
Product, Category 테이블 설계 질문드립니다 !
Category와 Product의 관계를 N:N 으로 설계하신 것은 향후 확장성이나 관계 변경을 고려하신 것일까요?개인적으로는 처음에 1:1 또는 1:N 구조로 설계해 한쪽 테이블에 외래키를 두는 경우가 많았는데,비즈니스 요구사항이 변하면서 결국 N:N 관계로 바뀌어 중간 매핑 테이블을 추가한 경험이 여러 번 있었습니다.그래서 이번 강의를 보며,“어차피 관계가 바뀔 가능성이 있다면 처음부터 매핑 테이블로 시작하는 게 더 유연하지 않을까?”라는 생각이 들었습니다.다만 그렇게 되면 Status(상태) 같은 중복 데이터가 생기기도 해서이런 트레이드오프에 대해 재민님은 어떤 기준으로 판단하셨는지도 궁금합니다.좋은 강의 감사합니다 :>
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
security 인증은 어떤 모듈에 놔두는게 좋을까요?
예전에 유튜브영상에서는 인증서버를 GW를 두거나 Oauth 서버를 따로 두는 방식을 선호한다고 하셨는다.현재 서버 분리를 하지 않기에, GW 나 Oauth도 사용하지 않기에 security 를 현재 사용중입니다.1. web:security 모듈로 빼두었습니다. storage,domain 모듈이 web모듈을 알지못합니다. 강의 에서도 인증이 없기에 어떻게 구성하실지 궁금합니다security를 지양하시는거는 알지만 써야하는 상황이고 구성한다면 어디에 빼두시는지 궁금합니다2. security는 presentation layer 에 속한다고 판단을 했는데, 그럼 user의 role에 따라 조회시 비즈니스레이어를 건너뛰고 filter 에서 userid 만 사용해서 security context 를 생성하고 있는데,이거는 레이어간 건너뛰기가 되어서 레이어 규칙 위반인데 예외적 허용을 하는게 맞는지 잘모르겠습니다.깊이 있는 질문을 하고 싶었는데, 얇은 질문이지만 남겨봅니다!!
-
해결됨프로덕션 레벨 실시간 채팅 서버 구축: 분산 처리부터 성능 최적화까지 (Kotlin & Spring)
웹소켓을 이용한 채팅시스템에서 부하테스트를 어떻게 진행해야할까요?
안녕하세요. 취준하고있는 예비 개발자입니다. 개인프로젝트를 진행하다가 여쭤보고 싶은게 있어 강의까지 구매하게 되었습니다. 가장 궁금한 질문은 '웹소켓을 이용한 채팅시스템에서 부하테스트를 어떻게 진행해야하는가?'입니다. 추가질문 및 부연설명을 위해 조금만 더 읽어주시면 감사하겠습니다. 현재 진행하고 있는 앱 개발 프로젝트 진행 중입니다.기술스택은 서버는 코틀린,스프링이고, 클라이언트는 iOS(swift)와 안드로이드(Kotlin)로 구성했습니다. http요청을 처리하는 서버(스프링 서버)는 단일 서버와 단일 데이터베이스로만 구성한 상황이고, 단일 인스턴스는 aws의 t2.micro를 이용하고 있습니다. 서버에는 nginx / next 서버(홍보용 홈페이지) / 스프링 서버 / github-runner 등의 프로세스가 실행 중에 있습니다. 클라이언트의 무한 재연결 로직의 문제로 인해 인스턴스 내부에서 'ss -s' 명령어를 이용해 소켓상태를 조회해본 결과, 소켓 tcp연결이 폭발적으로 증가하여 400개까지 증가한 상황이 있었습니다. 이 상황에서 소켓을 이용한 채팅뿐만 아니라 사용자 조회와 같은 http요청 모두 느려지는 것을 확인되었습니다. 하지만 재연결 로직을 수정하고 이후 tcp연결이 400(= 클라이언트 - nginx 200개 / nginx - 서버 200개)까지 증가하는 상황을 만들어봐야 또 문제가 발생하는지 확인할 수 있다고 생각했지만 200명의 테스터를 모을 수 없다고 생각했습니다. 또한, 채팅의 API를 하나 파고, ngrinder를 이용해 부하테스트를 요청하는 상황이 적합할까 생각했을 떄, 웹소켓 연결이 되지 않는 상황이라고 생각되어 적합하지 않다고 생각했습니다. 이런 상황에서 어떻게 테스트해볼 수 있을지 고민됩니다. 추가적으로 궁금한 점은앱 개발에서 채팅시스템을 구축하는 상황이고, 대략 500~1000명을 수용해야한다면 어떤 기술을 적용해 채팅시스템을 구축하셨을 것 같나요? 실시간 통신하면 웹소켓정도 밖에 모르는 상황이었기에 웹소켓을 적용했지만서도 타당했는가? 적합했는가에 대한 의문이 여전히 남아있는 상황이라고 생각하기 때문에 질문드렸습니다. t2.micro 서버는 얼만큼의 소켓연결까지 버틸 수 있는지 알고 싶습니다. 현재 서비스의 사전예약자가 80명 정도 되는 상황이라 t2.micro를 이용했을 때 서버가 터질까봐 우려스럽습니다. 그래서 서버 스펙을 확장을 고려하고 있는데, 취준생이기에 비용적인 측면에서 고려하지 않을 수가 없는 상황이라 '정말 확장하는게 맞을까?', '내가 능력이 부족한 게 아닐까?' 라는 생각이 들어 갈피를 못잡고 있는 상황이라 질문드렸습니다.제 질문들이 대부분 인프라 확장의 타당성을 갖추기 위한 질문이라고 생각합니다. 혹시 인프라 확장을 위한 근거로써 어떤 지표가 타당성을 확보할 수 있다고 생각하시는지 궁금합니다. 긴 질문 읽어주셔서 감사합니다.행복한 하루 되세요~
-
미해결[중급편] 코인 가격 모니터링 앱 제작 (Android Kotlin)
빌드가 안 돼요..
A problem occurred configuring root project 'coco2'.> java.util.concurrent.ExecutionException: org.gradle.api.GradleException: Failed to create Jar file C:\Users\dlekd\.gradle\caches\jars-9\18366b31678c0171857be093a3b8ec22\bcprov-jdk18on-1.79.jar.* Try:> Run with --info or --debug option to get more log output.> Run with --scan to get full insights.> Get more help at https://help.gradle.org.* Exception is:org.gradle.api.ProjectConfigurationException: A problem occurred configuring root project 'coco2'. at org.gradle.configuration.project.LifecycleProjectEvaluator.wrapException(LifecycleProjectEvaluator.java:84) at org.gradle.configuration.project.LifecycleProjectEvaluator.addConfigurationFailure(LifecycleProjectEvaluator.java:77) at org.gradle.configuration.project.LifecycleProjectEvaluator.access$400(LifecycleProjectEvaluator.java:55) at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.lambda$run$0(LifecycleProjectEvaluator.java:111) at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:395) at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$fromMutableState$2(DefaultProjectStateRegistry.java:418) at org.gradle.internal.work.DefaultWorkerLeaseService.withReplacedLocks(DefaultWorkerLeaseService.java:345) at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:418) at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:394) at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.run(LifecycleProjectEvaluator.java:100) at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29) at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26) at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157) at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47) at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68) at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:72) at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:782) at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:156) at org.gradle.api.internal.project.ProjectLifecycleController.lambda$ensureSelfConfigured$2(ProjectLifecycleController.java:84) at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255) at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266) at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254) at org.gradle.internal.model.StateTransitionController.lambda$maybeTransitionIfNotCurrentlyTransitioning$10(StateTransitionController.java:199) at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:34) at org.gradle.internal.model.StateTransitionController.maybeTransitionIfNotCurrentlyTransitioning(StateTransitionController.java:195) at org.gradle.api.internal.project.ProjectLifecycleController.ensureSelfConfigured(ProjectLifecycleController.java:84) at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.ensureConfigured(DefaultProjectStateRegistry.java:369) at org.gradle.execution.TaskPathProjectEvaluator.configure(TaskPathProjectEvaluator.java:33) at org.gradle.execution.TaskPathProjectEvaluator.configureHierarchy(TaskPathProjectEvaluator.java:47) at org.gradle.configuration.DefaultProjectsPreparer.prepareProjects(DefaultProjectsPreparer.java:42) at org.gradle.configuration.BuildTreePreparingProjectsPreparer.prepareProjects(BuildTreePreparingProjectsPreparer.java:65) at org.gradle.configuration.BuildOperationFiringProjectsPreparer$ConfigureBuild.run(BuildOperationFiringProjectsPreparer.java:52) at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29) at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26) at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157) at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47) at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68) at org.gradle.configuration.BuildOperationFiringProjectsPreparer.prepareProjects(BuildOperationFiringProjectsPreparer.java:40) at org.gradle.initialization.VintageBuildModelController.lambda$prepareProjects$2(VintageBuildModelController.java:84) at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255) at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266) at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254) at org.gradle.internal.model.StateTransitionController.lambda$transitionIfNotPreviously$11(StateTransitionController.java:213) at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:34) at org.gradle.internal.model.StateTransitionController.transitionIfNotPreviously(StateTransitionController.java:209) at org.gradle.initialization.VintageBuildModelController.prepareProjects(VintageBuildModelController.java:84) at org.gradle.initialization.VintageBuildModelController.getConfiguredModel(VintageBuildModelController.java:64) at org.gradle.internal.build.DefaultBuildLifecycleController.lambda$withProjectsConfigured$1(DefaultBuildLifecycleController.java:130) at org.gradle.internal.model.StateTransitionController.lambda$notInState$3(StateTransitionController.java:132) at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:44) at ...오류는 이렇게 뜨고요ㅠㅠㅠ 더 긴데 글자수 제한이 있어서.. jdk 파일을 21이상으로 다운 받으라는 거 같아서 그렇게 하는데 적용이 안 됩닏.. 도와주세요.........
-
미해결[중급편] 코인 가격 모니터링 앱 제작 (Android Kotlin)
압축 폴더 파일이 비어있어요ㅠ
이미지 파일과 로띠 파일 등이 들어 있는 자료 폴더가 비어있어요...
-
해결됨제미니의 개발실무 - 커머스 백엔드 기본편
외부 API 연동 시 데이터 정합성을 고려해야 할 때..
안녕하세요.저도 개인 프로젝트로 이커머스를 만들면서, 결제와 같은 외부 API를 호출하는 부분에 대해서 많은 고민을 하게 되는 것 같습니다.가령, 결제 승인을 위해 PG API를 호출한 이후 결제 내역을 DB에 저장한다던가..혹은 결제 처리 이후 진행되어야 할 비즈니스 로직(배송 생성, 재고 차감 등..)을 진행해야한다던가..하지만 외부 API를 호출한다는 것은 사실 비즈니스 로직의 트랜잭션과 묶일 수 없다는 것이 참 어려운 것 같습니다.PG API 호출에 성공했지만, 이후 비즈니스 로직이 실패된다면 결제 강의 - 코드느끼기에서 말씀해주신 것 처럼,사용자의 돈은 빠져나갔지만, 배송 처리가 되지 않거나 그런 일이 발생할 수 있을 것 같아요.혹은 PG API 호출 시에, 타임 아웃이 발생해서 실패했다고 판단했지만, 알고보니 PG 서버 상으로는 승인이 정상적으로 처리 된 경우도 있을 것 같아요.이처럼 외부 API 연동 시에 데이터 정합성을 고려하는 것이 엄청 어려운 것 같습니다. 그래서 저는 결제처럼 사용자의 돈을 처리해야하는 경우 세부적인 방어 로직이 필요할 것 같다는 생각을 합니다.그래서 고민을 하면서, 이것저것 찾아보다가 사용하게 된 패턴이 외부 API를 요청하기 전에, state를 추가해서 관리하자 라는 것이었는데요. 예를 들어 결제로직의 경우 결제 검증을 처리하고 승인 API를 호출하기 전에 Payment의 State를 PENDING_PG_REQUEST(예시)로 변경한 뒤, PG API를 호출하는 흐름입니다. 만약 PG API 호출에 정상적으로 성공했지만, 결제 후처리 비즈니스 로직에 실패했더라도,스케줄러 같은 걸 통해 특정 시간 동안 계속해서 PENDING_PG_REQUEST인 결제 건이 있다면, 이것은 적어도 PG API를 호출하고 나서, 무언가 잘못되었다는 것이니까, 데이터 정합성을 맞춰주기 위해 한번 더 직접 API를 호출하고 나서 비즈니스 로직을 추가적으로 실행시켜주는 그런 작업을 진행해주면 될 것 같아요.이런 패턴이 어떻게 보면 강의 코드 예제에서 정산을 처리하는 SettlementService의 transfer 메서드와 비슷하다고 생각합니다. Settlement Entity에 Ready인 데이터를 결국 주기적으로 처리하면서, 언젠가는 정산 처리를 진행하게 되니까요!근데 문득 제가 사용하는 방식, 그리고 transfer 메서드의 방식은 특정 조건이 충족해야한다는 점이 있는 것 같아요. 사용하는 외부 API가 멱등성을 제공해야하고, 데이터 정합성을 처리하기 위해 돌아가는 스케줄러,배치 또한 자체적으로 중복처리 방지를 방지해야 한다는 것을요...이렇게 생각하다보니 끝도 없이 딥해지고 복잡해지는 것 같아서, 문득 다시 결제 부분 강의를 보다보니,수기 처리 방식도 아주 잠깐 언급하셨더라구요. 문제가 발생했을 때 로그를 남겨놔서, 데이터를 비교 후 데이터 정합성을 맞춘다. 근데 돈과 관련된 부분은 그 즉시, 성공 실패에 대한 처리를 해줘야할 것 같기도 하고 이 부분만큼은 과하게 방어 로직을 작성하는 게 맞을까?라는 생각이 들게 되는 것 같아요. 그리고 만약 외부 API가 멱등성을 제공하지 않으면어떻게 처리해야하지?라는 생각이 들기도 하고요.재민님께서는, 이런 외부 API 연동과 데이터 정합성을 고려해야할 때 방어 로직을 깊게 생각하시는지 궁금해서 질문을 하게 되었습니다. 근데 사실 돈과 관련된 부분은 아무리 생각해도 많은 방어 로직을 필요로 하는 것은 당연한 것 같긴한데, 어느 정도로 처리를 해줘야할 지 모르겠네요. 하하..최소한의 방어로직, 그리고 예외, 실패 시 로깅 처리로 모든 가능성을 추적해야 하는 게 효율적일까요??감사합니다!