Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ dependencies {
// AWS S3 의존성
implementation 'software.amazon.awssdk:s3:2.17.70'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// 레디스 의존성
implementation 'org.redisson:redisson-spring-boot-starter:3.23.2'
}

tasks.named('test') {
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/com/brainpix/BrainpixApplication.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.brainpix;

import jakarta.annotation.PostConstruct;

import java.util.TimeZone;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;

import jakarta.annotation.PostConstruct;

@SpringBootApplication
@EnableMongoAuditing
@EnableMongoRepositories(basePackages = {"com.brainpix.message.repository", "com.brainpix.alarm.repository"})
@EnableScheduling
public class BrainpixApplication {

@PostConstruct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public enum KakaoPayErrorCode implements ErrorCode {
IDEA_OWNER_PURCHASE_INVALID(HttpStatus.BAD_REQUEST, "KAKAOPAY400", "글 작성자는 구매할 수 없습니다."),
QUANTITY_INVALID(HttpStatus.BAD_REQUEST, "KAKAOPAY400", "구매 수량은 재고를 초과할 수 없습니다."),

// 404 Not Found - 리소스를 찾을 수 없음
PAYMENT_DATA_NOT_FOUND(HttpStatus.NOT_FOUND, "KAKAOPAY404", "결제 정보를 찾을 수 없습니다."),

// 500 Internal Server Error - 서버 내부 오류
API_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "KAKAOPAY500", "카카오페이 API 호출 중 오류가 발생하였습니다.");

Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/brainpix/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.brainpix.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedisConfig {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private Integer port;

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + host + ":" + port) // TLS이면 rediss://
.setConnectionMinimumIdleSize(10)
.setConnectionPoolSize(50);
return Redisson.create(config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.entity.KakaoPaymentData;
import com.brainpix.kakaopay.dto.KakaoPaymentDataDto;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

Expand Down Expand Up @@ -51,7 +51,7 @@ public KakaoPayReadyDto.KakaoApiResponse requestPaymentReady(

// 카카오페이 결제 승인 API
public KakaoPayApproveDto.KakaoApiResponse requestPaymentApprove(
KakaoPayApproveDto.Parameter parameter, KakaoPaymentData kakaoPaymentData) {
KakaoPayApproveDto.Parameter parameter, KakaoPaymentDataDto kakaoPaymentData) {

Map<String, String> parameters = getApproveParams(parameter, kakaoPaymentData);
HttpHeaders headers = getHeaders();
Expand Down Expand Up @@ -89,14 +89,14 @@ private Map<String, String> getReadyParams(

// 결제 승인 파라미터 생성
private Map<String, String> getApproveParams(KakaoPayApproveDto.Parameter parameter,
KakaoPaymentData kakaoPaymentData) {
KakaoPaymentDataDto kakaoPaymentData) {

Map<String, String> params = new HashMap<>();

params.put("cid", cid);
params.put("tid", kakaoPaymentData.getTid());
params.put("partner_order_id", parameter.getOrderId());
params.put("partner_user_id", String.valueOf(kakaoPaymentData.getBuyer().getId()));
params.put("partner_user_id", String.valueOf(kakaoPaymentData.getBuyerId()));
params.put("pg_token", parameter.getPgToken());

return params;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class KakaoPayController {
private final KakaoPayFacadeService kakaoPayService;

@AllUser
@Operation(summary = "결제 준비 API", description = "아이디어 식별자, 판매자 식별자, 상품 수량, 총 결제금액(vat 포함), vat를 json 본문으로 입력받아 카카오페이 쪽에 결제 정보를 생성합니다.<br>사용자를 결제 화면으로 이동시킬 수 있도록 리다이렉트 url이 응답값으로 제공됩니다.<br>해당 url로 이동하여 비밀번호 입력을 마치면 프론트 쪽 경로로 이동합니다. 이때 쿼리 파라미터로 전달되는 pgToken, ideaId, orderId를 /kakao-pay/approve에 전달하시면 됩니다.")
@Operation(summary = "결제 준비 API", description = "아이디어 식별자, 판매자 식별자, 상품 수량, 총 결제금액(vat 포함), vat를 json 본문으로 입력받아 카카오페이 쪽에 결제 정보를 생성합니다.<br>사용자를 결제 화면으로 이동시킬 수 있도록 리다이렉트 url과 주문 번호가 응답값으로 제공됩니다.<br>주문 번호는 이후 승인 단계에서 pgToken과 함께 전달해주시면 됩니다.")
@PostMapping("/ready")
public ResponseEntity<ApiResponse<KakaoPayReadyDto.Response>> kakaoPayReady(@UserId Long userId,
@RequestBody KakaoPayReadyDto.Request request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static KakaoPayApproveDto.Parameter toParameter(Long userId, KakaoPayAppr
.userId(userId)
.orderId(request.getOrderId())
.pgToken(request.getPgToken())
.ideaId(request.getIdeaId())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.brainpix.kakaopay.converter;

import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.entity.KakaoPaymentData;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

public class KakaoPayReadyDtoConverter {

Expand All @@ -27,18 +24,4 @@ public static KakaoPayReadyDto.Response toResponse(KakaoPayReadyDto.KakaoApiResp
.orderId(orderId)
.build();
}

public static KakaoPaymentData toKakaoPaymentData(KakaoPayReadyDto.KakaoApiResponse kakaoApiResponse,
KakaoPayReadyDto.Parameter parameter,
String orderId, User buyer,
IdeaMarket ideaMarket) {

return KakaoPaymentData.builder()
.tid(kakaoApiResponse.getTid())
.quantity(parameter.getQuantity())
.orderId(orderId)
.buyer(buyer)
.ideaMarket(ideaMarket)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.brainpix.kakaopay.converter;

import com.brainpix.kakaopay.dto.KakaoPaymentDataDto;

public class KakaoPaymentDataDtoConverter {

public static KakaoPaymentDataDto toKakaoPaymentDataDto(String tid, Long buyerId, Long quantity) {

return KakaoPaymentDataDto.builder()
.tid(tid)
.buyerId(buyerId)
.quantity(quantity)
.build();
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/brainpix/kakaopay/dto/KakaoPaymentDataDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.brainpix.kakaopay.dto;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class KakaoPaymentDataDto {

private String tid; // 카카오페이 쪽 주문 고유 번호
private Long buyerId; // 유저 ID
private Long quantity; // 구매 수량
}
43 changes: 0 additions & 43 deletions src/main/java/com/brainpix/kakaopay/entity/KakaoPaymentData.java

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.brainpix.kakaopay.service;

import java.util.concurrent.TimeUnit;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.brainpix.api.code.error.CommonErrorCode;
import com.brainpix.api.exception.BrainPixException;
import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.repository.NamedLockRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -17,35 +20,35 @@
@RequiredArgsConstructor
public class KakaoPayFacadeService {

private final NamedLockRepository namedLockRepository;
private final KakaoPayService kakaoPayService;
private final RedissonClient redissonClient;

@Transactional
public KakaoPayReadyDto.Response kakaoPayReady(KakaoPayReadyDto.Parameter parameter) {
return kakaoPayService.kakaoPayReady(parameter);
}

@Transactional
// 락 획득 → 트랜잭션 시작 → 트랜잭션 해제 → 락 해제 (락 해제가 커밋 이후로 보장되어야 하므로 Facade 패턴 적용)
public KakaoPayApproveDto.Response kakaoPayApprove(KakaoPayApproveDto.Parameter parameter) {
String lockName = "LOCK_KAKAO_PAY_APPROVE_" + parameter.getIdeaId();
try {
acquireLock(lockName);
return kakaoPayService.kakaoPayApprove(parameter);
} finally {
releaseLock(lockName);
}
}

private void acquireLock(String lockName) {
log.info("락 요청 lockName : {}", lockName);
Integer result = namedLockRepository.getLock(lockName);
if (result == null || result == 0) {
String lockKey = "lock:idea:" + parameter.getIdeaId();
RLock lock = redissonClient.getLock(lockKey);
boolean isLocked = false;

try {
isLocked = lock.tryLock(5, TimeUnit.SECONDS); // 5초동안 락 획득 대기
if (isLocked) {
log.info("결제 락 획득 성공: {}", lockKey);
return kakaoPayService.kakaoPayApprove(parameter); // 승인 서비스 코드 실행
} else {
log.info("결제 락 획득 실패: {}", lockKey);
throw new BrainPixException(CommonErrorCode.INTERNAL_SERVER_ERROR);
}
} catch (InterruptedException e) {
throw new BrainPixException(CommonErrorCode.INTERNAL_SERVER_ERROR);
} finally {
lock.unlock(); // 락 해제
log.info("결제 락 해제 성공: {}", lockKey);
}
}

private void releaseLock(String lockName) {
log.info("락 반환 lockName : {}", lockName);
namedLockRepository.releaseLock(lockName);
}
}
Loading