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
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,46 @@ simplepg/
- **`payment` → `payment_log`**: 1:N 관계 (FK : `payment_id`, `ON DELETE CASCADE`)

---
## 테스트 방법
## 🧪 테스트 방법

### 🌐 JSP 테스트 페이지 사용법

애플리케이션 실행 후 브라우저에서 다음 URL로 접속하여 테스트할 수 있습니다

#### 📊 메인 대시보드
```
http://localhost:8080/
```
- 시스템 상태 확인
- 각 테스트 페이지로 이동하는 네비게이션 제공

#### 💳 결제 시스템 테스트
```
http://localhost:8080/payment-test
```
- **결제 요청**: 새로운 결제 요청 생성 및 Payment Key 발급
- **결제 상태 조회**: Payment Key로 결제 상태 확인
- **결제 취소**: 결제 취소 처리 (사유 포함)
- **결제 완료**: 결제 완료 처리
- **최근 결제 내역**: Mock 데이터로 결제 내역 확인

#### 🔄 웹훅 테스트
```
http://localhost:8080/webhook-test
```
- **웹훅 플로우 시각화**: 결제 처리 과정을 단계별로 표시
- **성공 웹훅 전송**: 외부 결제 시스템에서 성공 결과 전송 시뮬레이션
- **실패 웹훅 전송**: 외부 결제 시스템에서 실패 결과 전송 시뮬레이션
- **Mock 서버 요청**: Mock 서버로 결제 요청 전송
- **실시간 로그**: 웹훅 전송 과정을 실시간으로 모니터링

#### 🔧 API 테스트
```
http://localhost:8080/api-test
```
- **빠른 테스트**: 간단한 결제 요청 및 상태 조회
- **JSON 요청 테스트**: 커스텀 JSON으로 API 직접 호출
- **응답 분석**: API 응답 결과 분석 및 표시

### 🔐 보안 고려사항
- 실제 설정 파일들은 `.gitignore`에 의해 제외됩니다
Expand Down
8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ext {
junitVersion = '5.9.2'
hikariVersion = '4.0.3'
jstlVersion = '1.2'
retryVersion = '1.3.4'
}

dependencies {
Expand All @@ -37,6 +38,10 @@ dependencies {
implementation "org.springframework:spring-tx:${springVersion}"
testImplementation "org.springframework:spring-test:${springVersion}"

// Spring retry
implementation "org.springframework.retry:spring-retry:${retryVersion}"
implementation "org.springframework:spring-aspects:${springVersion}"

// Spring Webflux
implementation 'io.projectreactor:reactor-core:3.4.0'
implementation 'org.springframework:spring-webflux:5.3.10'
Expand Down Expand Up @@ -81,9 +86,10 @@ dependencies {
implementation "commons-codec:commons-codec:1.15"

// Servlet & JSP
implementation "javax.servlet:javax.servlet-api:3.1.0"
implementation "javax.servlet:javax.servlet-api:4.0.1"
compileOnly "javax.servlet.jsp:jsp-api:2.2"
implementation "javax.servlet:jstl:${jstlVersion}"
providedRuntime 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.75'

// 보안
implementation("org.springframework.security:spring-security-web:${springSecurityVersion}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.jaesung.simplepg.common.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import me.jaesung.simplepg.domain.dto.webhook.MerchantRequest;

@Getter
@AllArgsConstructor
public class PaymentProcessedEvent {

private final MerchantRequest merchantRequest;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package me.jaesung.simplepg.common.event.handler;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.jaesung.simplepg.common.event.PaymentProcessedEvent;
import me.jaesung.simplepg.service.webclient.WebClientService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@Slf4j
@RequiredArgsConstructor
public class PaymentEventHandler {

private final WebClientService webClientService;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handlePaymentEvent(PaymentProcessedEvent event) {
handlePaymentEventAsync(event);
}

@Async
@Retryable(value = {Exception.class}, backoff = @Backoff(delay = 3000))
public void handlePaymentEventAsync(PaymentProcessedEvent event) {
try {
log.info("Payment 정상 커밋 완료 후 비동기 요청 실행: {}", event.getMerchantRequest().getPaymentKey());
webClientService.sendResponse(event.getMerchantRequest());
} catch (Exception e) {
log.error("Payment -> Client 결제 정보 전송 실패 및 재시도 예정: {}", event.getMerchantRequest().getPaymentKey(), e);
throw e;
}
}

@Recover
public void retryFailed(Exception e, PaymentProcessedEvent event) {
log.error("Payment -> Client 결제 정보 최종 실패: {}", event.getMerchantRequest().getPaymentKey(), e);
}
}
28 changes: 17 additions & 11 deletions src/main/java/me/jaesung/simplepg/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.*;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

Expand All @@ -28,9 +31,10 @@
@PropertySource(value = "classpath:application-${spring.profiles.active}.properties",
ignoreResourceNotFound = true)
@MapperScan(basePackages = "me.jaesung.simplepg.mapper")
@ComponentScan(basePackages = "me.jaesung.simplepg.**")
@EnableTransactionManagement
public class AppConfig {
@ComponentScan(basePackages = "me.jaesung.simplepg.**",
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class))
@EnableTransactionManagement(mode = AdviceMode.PROXY, proxyTargetClass = true)
public class AppConfig implements AsyncConfigurer {

@Value("${spring.datasource.driver-class-name}")
String driver;
Expand Down Expand Up @@ -66,12 +70,6 @@ public SqlSessionFactory sqlSessionFactory() throws Exception {
return sqlSessionFactory.getObject();
}

@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}


@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
Expand All @@ -90,6 +88,14 @@ public WebClient webClient() {
.build();
}

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Bean
public ApplicationEventPublisher applicationEventPublisher(ApplicationContext applicationContext) {
return applicationContext;
}

}
34 changes: 34 additions & 0 deletions src/main/java/me/jaesung/simplepg/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package me.jaesung.simplepg.config;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
@EnableRetry
public class AsyncConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}

}
27 changes: 19 additions & 8 deletions src/main/java/me/jaesung/simplepg/config/ServletConfig.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
package me.jaesung.simplepg.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.*;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@EnableWebMvc
@Configuration
@PropertySource({"classpath:/application.properties"})
@PropertySource(value = "classpath:application-${spring.profiles.active}.properties",
ignoreResourceNotFound = true)
@ComponentScan(basePackages = "me.jaesung.simplepg.controller")
@ComponentScan(basePackages = "me.jaesung.simplepg.service")
public class ServletConfig implements WebMvcConfigurer {

@Value("${server.URI}")
@Value("${server.URI:http://localhost:8080}")
String base_uri;

@Override
Expand All @@ -27,9 +25,10 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
.addResourceLocations("/resources/");
registry.addResourceHandler("/assets/**")
.addResourceLocations("/resources/assets/");
registry.addResourceHandler("/static/**")
.addResourceLocations("/static/");
}


@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
Expand All @@ -38,9 +37,21 @@ public void addCorsMappings(CorsRegistry registry) {
.allowedHeaders("*");
}

/**
* JSP 뷰 리졸버 설정
*/
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/jsp/");
resolver.setSuffix(".jsp");
resolver.setViewClass(JstlView.class);
resolver.setOrder(1);
registry.viewResolver(resolver);
}

@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}

}
17 changes: 14 additions & 3 deletions src/main/java/me/jaesung/simplepg/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package me.jaesung.simplepg.config;


import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import javax.servlet.Filter;

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{AppConfig.class, SecurityConfig.class};
return new Class[]{AppConfig.class, SecurityConfig.class, AsyncConfig.class};
}

@Override
Expand All @@ -20,5 +22,14 @@ protected String[] getServletMappings() {
return new String[]{"/"};
}


/**
* Character Encoding Filter 설정
*/
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
return new Filter[]{characterEncodingFilter};
}
}
Loading
Loading