diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md index ebeffaf..6d7f68d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,705 @@ # spring-tutorial-21st -CEOS back-end 21st spring tutorial project + +
+면접용 정리 - 경어체로 작성
+나만의 정리 - 평어체로 작성
+ +이 글의 하이라이트는 맨 마지막에 있습니다 + + +## 1. Spring Framework + +Spring이 지원하는 다양한 기술들을 알아보자. + + +### Ioc / DI / IoC Container + +**IoC란?** +- IoC란 객체 생성, 생명주기 제어를 개발자가 아닌 외부에 위임하는 것을 뜻합니다.
+ Spring에서는 DI를 통해 IoC를 구현합니다. + +**IoC Container란?** +- IoC 컨테이너란 Spring에서 bean을 생성하고 의존성 및 생명주기를 관리하는 컨테이너 입니다.
+ 개발자는 DI 방식으로 bean을 주입받아 사용합니다. + +**DI란?** +- DI란 객체를 직접 생성하지 않고 외부에서 받아 사용하는 것을 뜻합니다.
+ Spring에서는 스프링 IoC 컨테이너가 객체를 생성하고 의존성을 주입합니다. + +**DI 종류?** +- DI 종류는 3가지 setter 주입, 필드 주입, 생성자 주입이 있습니다.
+ 안정성 문제로 setter 지양
+ 필드 주입은 테스트의 어려움
+ 등의 이유로 생성자 주입이 가장 권장됩니다. + +(final 선언 가능, 순환 참조 언급도 좋음) + +**필드 주입은 왜 테스트가 어려움?** +- 필드 주입은 프레임워크에 가장 큰 의존성을 가지기 때문에 테스트가 어렵습니다.
+ 구체적으로 Spring 컨테이너 없이 JUnit에서 테스트 하기 어렵습니다. + + +**DI가 그래서 왜 좋은데?** +- 클래스간 결합을 느슨하게 할 수 있습니다. 이로인해 의존성이 줄어들고, 재사용성이 높아집니다.
+ 예를 들어 service에서 repository 구현체를 의존하면, 구현체가 바뀔때마다 service의 코드를 수정해야합니다.
+ 하지만 service에서 repository의 interface를 의존했다면 주입해주는 구현체만 바꾸면 되기 때문에 재사용성이 높아집니다. + + +**생성자 주입시 왜 private final?** +- final 키워드는 생성시 불변, 선언시 초기화를 시켜주는 것을 강제합니다.
+ 선언시 초기화 시켜주는 것을 강제해서 스프링이 컨테이너에 있는 bean을 주입해 줄 수 있습니다. + + +### JPA + +**JPA란?** +- JPA는 Java Persistence API의 약자로 자바 ORM 기술 표준입니다.
+ ORM이란 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 기술입니다. + +**N+1 문제?** +- 근본적인 원인은 관계형 데이터베이스와 객체지향 언어간 차이때문에 발생합니다.
+ 객체는 연관관계가 있다면 메모리 내에서 접근할 수 있지만
+ 데이터베이스는 select 쿼리로만 조회할 수 있기 때문입니다. + +**해결은?** +- fetch join으로 해결할 수 있습니다.
+ 하지만 페이징시, to many 관계에서 문제가 발생할 수 있습니다. + + +**어떤 문제가 발생하는데?** +- out of memory 문제가 발생할 수 있습니다
+ JPA는 어떤 데이터를 기준으로 paging할지 모르기 때문에
+ 모든 데이터를 메모리에 올리고 paging을 수행합니다.
+ 이를 해결하기 위해 batch size를 설정할 수 있습니다. + +**영속성 컨텍스트란?** +- 엔티티를 영구히 저장하는 환경을 뜻합니다
+장점으로는 1차캐시, 쓰기 지연, 변경 감지, 지연 로딩, 동일성 보장이 있습니다. + + +**1차캐시란?** +- 조회가 가능한 객체이며 없다면 db에서 조회합니다. + +**쓰기 지연이란?** +- 트랜잭션을 지원하는 기능으로 쿼리를 바로 보내는 것이 아니라 모아서 보낼 수 있습니다 + +**변경감지란?** +- 1차캐시 데이터를 스냅샷 찍고, 커밋 시점의 엔티티와 비교해 update sql을 생성합니다 + +**지연로딩이란?** +- 연관관계가 있는 엔티티를 바로 read하는게 아니라 접근할때 쿼리를 보내 로드하는 것입니다.
+이때 사용하는 것이 프록시 객체입니다 + +**프록시 객체가 뭔데?** +- 지연 로딩과 관련 있습니다.
+영속성 컨텍스트는 연관관계가 있는 객체를 데이터베이스에서 바로 가져오는 것이 아니라
+가짜 객체인 프록시 객체로 참조하고 있다가 그 값을 사용하려 할 때 데이터 베이스에 접근합니다. + +**동일성은 뭐고 동등성이랑 뭐가 달라?** +- 동일성은 객체의 주소값이 같은지를 비교하는 것이고
+ 동등성은 객체의 값이 같은지를 비교하는 것입니다 + +### PSA (Portable Service Abstraction) + +추상화를 사용해 기술을 내부에 숨겨 개발자에게 편의성을 제공하는 것이 Service Abstraction.
+환경에 상관 없이 **일관성 있게** 기술을 사용할 수 있도록 제공하는 것이 Portable Service Abstraction.
+ +예를 들어 + +DB에 접근하기 위해 JDBC를 사용할수도, JPA를 사용할수도 있다.
+하지만 **두 경우 모두** @Transactional 어노테이션 사용 가능. + +구체적으로 + +아래 그림처럼 +@Transactional 어노테이션은
+PlatFormTransactionManager **Interface에** 의존 + +Image + +PlatformTransactionManager 구현체를 살펴보면
+Image + +
+ +간단하게 코딩을 해보자.
+ +가짜 TransactionManager 2개를 만들고
+TransactionManager 인터페이스에 의존하는 서비스에서
+다른 구현체를 주입받고
+@Transactional 어노테이션을 사용해보겠다. + +
+ +가짜 트랜잭션 매니저를 하나 만들어 보고 +```java +public class DummyTransactionManager implements PlatformTransactionManager { + + private final String name; + + public DummyTransactionManager(String name) { + this.name = name; + } + + ... +} +``` + +인터페이스에 의존하는 Service
+메서드에 Transactional 어노테이션을 달아줬다. + +```java +@Service +@RequiredArgsConstructor +public class CompareTransactionService { + + private final PlatformTransactionManager transactionManager; + + @Transactional + public void executeTransactional(String managerName, Runnable action) { + + TransactionTemplate txTemplate = new TransactionTemplate(transactionManager); + txTemplate.execute(status -> { + action.run(); + return null; + }); + } +} +``` + + +단위테스트 +```java + + @Test + public void testJpaTransactionManager() { + // "JPA" DummyTransactionManager 주입 + DummyTransactionManager jpaTM = new DummyTransactionManager("JPA"); + CompareTransactionService service = new CompareTransactionService(jpaTM); + + System.out.println("== JPA Transaction Manager Test 시작 =="); + service.executeTransactional("JPA", () -> { + System.out.println("Action executed using JPA TM"); + }); + System.out.println("== JPA Transaction Manager Test 종료 =="); + } + + @Test + public void testJdbcTransactionManager() { + // "JDBC" DummyTransactionManager 주입 + DummyTransactionManager jdbcTM = new DummyTransactionManager("JDBC"); + CompareTransactionService service = new CompareTransactionService(jdbcTM); + + System.out.println("== JDBC Transaction Manager Test 시작 =="); + service.executeTransactional("JDBC", () -> { + System.out.println("Action executed using JDBC TM"); + }); + System.out.println("== JDBC Transaction Manager Test 종료 =="); + } +``` +실행결과 +``` +== JPA Transaction Manager Test 시작 == +[JPA] Transaction started. +Action executed using JPA TM +[JPA] Transaction committed. +== JPA Transaction Manager Test 종료 == +== JDBC Transaction Manager Test 시작 == +[JDBC] Transaction started. +Action executed using JDBC TM +[JDBC] Transaction committed. +== JDBC Transaction Manager Test 종료 == +``` + +실제 TransactionManager는 훨씬 복잡하겠지만
+어쨋든 두개의 구현체 모두 @Transactional 어노테이션을 사용할 수 있는걸 확인했다. + +
+ + +위에서 알아본 DI가 빛을 발하는 순간.
+ + 뒤에서 알아볼 AOP도 적용된 어노테이션이다. + + + +Spring Batch에서 TransactionManager를 직접 설정했던 작업,
+다중 DB를 구축할때 직접 주입했던 작업이 떠오르며
+조금 더 명확하게 이해가 되었다. + + +좋았던 글
+ + + + +### 🔥 AOP + +AOP란 비즈니스 로직에 공통적으로 적용되는 기능을 분리하여 관리하는 것을 뜻한다.
+특히 인증, 로깅, 트랜잭션에서 유용하다. + +구체적으로 + +애플리케이션 로직은 **핵심 기능**과 **부가 기능**으로 나눌 수 있다.
+그리고 부가 기능은 보통
+여러 곳에서 동일하게 사용된다.
+그리고 이를 횡단 관심사라고 한다. + +![Image](https://github.com/user-attachments/assets/19570869-0f24-4b67-bdff-2a89db0d90ee) + +사진에 있는 로그 추적 기능을 100개의 클래스에서 사용하면
+100개의 클래스에 로그 추적 코드를 넣어야 한다.
+그리고 로그 추적 기능을 변경한다면
+100개의 클래스를 수정해야 한다. + +AOP는 이런 문제를 해결하기 위해 등장했다.
+ +![Image](https://github.com/user-attachments/assets/3074b331-dcce-426a-9e0b-45bfee861ef0) + + +
+ +우선 Spring에서 AOP를 적용하는 방식을 **이론적으로** 알아보자.
+지금은 용어 정리, 원리를 간단하게 파악하고
+ +어노테이션에 대해 알아본 후
+**실질적으로** 적용하는 것은 마지막에.
+ + + +### AOP 적용 방식 + +3가지 방식이 있다. +- 컴파일 시점 (weaving) +- 클래스 로딩 시점 (weaving) +- 런타임 시점 (프록시) + +이것만 해도 방대한 양이고
+완벽히 이해하지 못해서 결론만 적겠다.
+ +AOP의 대표적인 구현으로 AspectJ 프레임워크가 있다.
+그리고 컴파일 시점, 클래스 로딩 시점에 적용하는 AOP 방식은 AspectJ를 직접 사용해야 한다.
+ +구체적으로 JAVA를 실행할때 복잡한 옵션을 걸어주거나
+클래스 로더 조작기를 설정해야한다.
+(어렵다는 뜻)
+ + +Spring 컨테이너, DI, 프록시, bean post processor 개념을 모두 사용해
+쉽게 사용할 수 있도록 만들어 놓은 것이 Spring AOP이고
+프록시를 사용한 런타임 시점에 적용하는 방식을 사용한다.
+ +정확하게 AspectJ 문법을 차용하고, 프록시 방식으로 AOP를 적용한다.
+ + +![Image](https://github.com/user-attachments/assets/39fb5984-8725-4d5f-97c2-9463d5ed0dcb) +![Image](https://github.com/user-attachments/assets/a1c8e3e8-da94-42be-8786-c3c36be9192c) + + +결론은
+쉽게 사용할 수 있도록 만들어 놓은거 쓰자. + +### Spring AOP 용어 정리 + + +![Image](https://github.com/user-attachments/assets/328b966f-c2bd-43e9-9d3e-e22e6007aa99) + + +알아야 할것만 정리해 보면
+- JoinPoint : 어드바이스가 적용될 수 있는 지점 (메서드 실행 지점) +- Pointcut : JoinPoint 중 실제로 어드바이스가 적용되는 지점 +- Advice : 실제로 어드바이스가 하는 일
+ 커스텀 예외 처리 해봤으면 RestControllerAdvice를 본적이 있을거다 + + +어드바이스 종류 +- @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외변환 등이 가능 +- @Before:: 조인 포인트 실행 이전에 실행 +- @AfterReturning: 조인 포인트가 정상 완료후 실행 +- @AfterThrowing: 메서드가 예외를 던지는 경우 실행 +- @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally) + +## 2. Spring Bean, Life Cycle + +Spring Bean이란? +- IoC 컨테이너 안에 들어있는 객체를 spring bean이라고 합니다
+ 개발자는 이 bean 객체를 DI로 주입받아 사용가능합니다. + +Bean Scope란? +- 스프링은 빈이라는 개념으로 객체를 관리합니다
+ 이 객체들의 생명주기를 Ioc 컨테이너에서 관리하는데
+ 이 범위를 bean scope라고 합니다
+ 추가적으로 빈 스코프 기본 전략은 싱글톤입니다. + +싱글톤이 뭔데? +- 하나의 객체를 공유하는 디자인 패턴입니다
+ 이를 사용할 때 같은 객체를 공유하기 때문에 값도 공유한다는 점을 유의해 개발해야 합니다 + +싱글톤이 뭐가 좋은데? +- DI를 사용하며 주입받은 객체를 계속해서 생성한다면 많은 메모리를 차지할 것입니다
+ 하지만 싱글톤은 객체를 한번만 생성하고 계속해서 사용하기 때문에 메모리를 효율적으로 사용할 수 있습니다 + + +## 🔥 3. Spring Annotation + +annotation이란 코드에 부가적인 기능을 수행하도록 하는 기술.
+Spring에서 다양한 설정을 간편하게 처리하는데,
+주로 reflection, proxy패턴을 사용해 동작을 구현한다. + +프록시 패턴은
+JPA 프록시 객체, 프록시 서버를 들어봤을 텐데 이와 비슷한 개념이다.
+간단하게 가짜 참조, 즉 대리자를 이용하는 거라고 생각하면 될 듯. + +reflection, proxy만 해도 너무 방대한 양이고
+설명할 정도까지 이해를 못해서 넘어가겠다. + +### 커스텀 어노테이션 + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Trace { + +} +``` + +@Target +Annotation이 어디에 위치할 수 있는지 제한한다 +- ElementType.FIELD : 클래스의 필드에 적용 +- ElementType.METHOD : 메서드에 적용 +- ElementType.CONSTRUCTOR : 생성자에 적용 + +@Retention +Annotation의 유지 기간을 정의한다. +- RetentionPolicy.SOURCE : runtime때 제거 +- RetentionPolicy.CLASS : (디폴트) 컴파일 후 .class 파일에 유지, runtime때 제거 +- RetentionPolicy.RUNTIME : runtime때 유지 + + + + + +## 4. Unit Test / Integration Test + +단위 테스트는 전체 코드 중 작은 부분을 테스트하는 것이다.
+통합 테스트는 시스템들이 서로 어떻게 상호작용하고 제대로 작동하는지 테스트하는 것을 의미한다. + +실질적으로 어떻게 작성하는지 보자 + + +대부분 작성해본 테스트 코드가 Unit Test일거다.
+test 환경 데이터베이스는 따로 설정하는 경우도 있고
+Mock 객체를 사용하는 경우도 있고
+데이터가 아직 안들어가 있다면 그냥 테스트를 돌리는 경우도 있다. + +개인적으로 테스트환경용
+h2 인메모리 데이터베이스를 설정하는게 좋다고 생각한다.
+![Image](https://github.com/user-attachments/assets/357f889c-9d46-44f2-8d05-57bf0acb72b0)
+간단하게 테스트 쪽에도 환경변수 설정해주면 된다 +```java + @Test + @DisplayName("모든 로또 조회") + void findAllLottos() { + // given + Lotto lotto = new Lotto(Randoms.pickUniqueNumbersInRange(1, 45, 6)); + // when + lottoRepository.addLotto(lotto); + // then + assertThat(lottoRepository.findAllLottos()).contains(lotto); + assertThat(lottoRepository.getLottoCount()).isEqualTo(1); + + } +``` + + +튜토리얼에 나온 코드가 통합 테스트이다.
+mock request를 보내 응답을 확인 +```java +@SpringBootTest +@AutoConfigureMockMvc +class HelloControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("HelloController 호출 시 Greetings 메세지 출력") + void getHello() throws Exception { + // given + // when + // then + mockMvc.perform(MockMvcRequestBuilders.get("/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("Greetings from Spring Boot!")); + + } + +} +``` + +## 5. 🔥 Spring AOP + Annotation 응용 🔥 + +가상의 상황을 생각해 보고
+그 상황을 해결하기 위해 AOP를 사용해 보자. + +### RETRY + +응답 처리 중
+외부 API를 호출하는 로직 있다고 가정하자.
+그런데 5번 요청 중 1번 꼴로 요청이 실패하면 어떻게 해결해야 할까?
+ +~~그런 경험이 있는지 생각해보니
+Open AI API를 사용할때 30번에 1번정도 실패했던 것 같다.
~~ + +#### 문제상황 코딩 + +5번에 한번 DB에 저장하는 작업이 실패 +```java +@Repository +public class ExamRepository { + + private static int sequence = 0; + + public String save(String itemId) { + sequence++; + if (sequence % 5 == 0) { + throw new IllegalArgumentException("Invalid item id: " + itemId); + } + return "ok"; + } + +} +``` +```java +@Service +@RequiredArgsConstructor +public class ExamService { + + private final ExamRepository examRepository; + + public void request(String itemId) { + examRepository.save(itemId); + } +} +``` + +테스트 코드 +```java +@SpringBootTest +@Slf4j +class ExamServiceTest { + + @Autowired + ExamService examService; + + @Test + void test(){ + for (int i = 1; i < 6; i++) { + log.info("client request: {}", i); + examService.request("item" + i); + } + } + +} +``` + +![Image](https://github.com/user-attachments/assets/1329af92-c58e-4987-bca9-f0124cbdd91b) + +당연히 5번째 작업에서 실패한다 + +#### 해결 + +실패했을 경우
+다시 시도하는 로직을 만들면 해결할 수 있을 것 같다. + +그리고 + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Retry { + + int value() default 3; + +} +``` + +```java +@Slf4j +@Aspect +public class RetryAspect { + + @Around("@annotation(retry)") + public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable { + log.info("[retry] {} retry: {}", joinPoint.getSignature(), retry); + + int maxValue = retry.value(); + Exception exceptionHolder = null; + + for (int retryCount = 1; retryCount < maxValue; retryCount++) { + try { + log.info("[retry] trying {} times, Max retry: {}", retryCount, maxValue); + return joinPoint.proceed(); + } catch (Exception e) { + exceptionHolder = e; + } + } + throw exceptionHolder; + } + + +} +``` + +@Retry 추가 +```java +@Repository +public class ExamRepository { + + private static int sequence = 0; + + + @Trace + @Retry(value = 4) + public String save(String itemId) { + sequence++; + if (sequence % 5 == 0) { + throw new IllegalArgumentException("Invalid item id: " + itemId); + } + return "ok"; + } + +} +``` + +재시도 로직을 만들어서 성공
+![Image](https://github.com/user-attachments/assets/3e731a94-850b-4219-834c-e3d628906f56) + +![Image](https://github.com/user-attachments/assets/43158bb5-503a-4a4f-bdf0-9a7857bf0354) + +실제 상황에서 꽤 유용할 것 같다. + + +### 사용자 구분 LOGGING + +실제 운영 환경에서는
+동시에 요청이 오는데, 이때 로그는 순차적으로 쌓이지 않는다. + +![Image](https://github.com/user-attachments/assets/89d5f10e-1812-43be-ae2c-e4165f9a479a) + +이를 해결하기 위해 MDC를 사용할 수 있다.
+MDC란 Mapped Diagnostic Context로
+Map 형식을 사용해 클라이언트 특징적인 데이터를 저장하기 위한 메커니즘이다. + +추가적으로
+Slf4j, logback 등 우리가 사용하는 로거에서 MDC를 지원한다.
+ +간단하게 코딩을 해보자 +MDC 설정 후 AOP 적용, 필터 적용 2가지 방법 모두 해보겠다.
+ +우선 출력 형식 설정
+logback.xml +```xml + + + + [%d{yyyy.MM.dd HH:mm:ss.SSS}] - [%-5level] - [%X{request_id}] - [%logger{5}] - %msg%n + + + + + + + +``` + +#### AOP 적용 + + +```java +@Slf4j +@Aspect +@Component +public class TestAspect { + + @Pointcut("execution(* com.ceos21.spring_boot.controller.*.*(..))") + public void controllerAdvice() { + + } + + @Before("controllerAdvice()") + public void requestLogging(JoinPoint joinPoint) { + MDC.put("traceId", UUID.randomUUID().toString()); + + log.info("REQUEST TRACING_ID : {}", MDC.get("traceId")); + } + + @AfterReturning(pointcut = "controllerAdvice()", returning = "result") + public void responseLogging(JoinPoint joinPoint, Object result) { + log.info("RESPONSE TRACING_ID : {}", MDC.get("traceId")); + MDC.clear(); + } +} + +``` + +![Image](https://github.com/user-attachments/assets/fa5d6ef9-4e27-478e-be3a-18891d8f7b58) + + +똑똑한 인텔리제이가 AOP advice를 적용했다고 알려준다. + +
+ + +적용 전 +![Image](https://github.com/user-attachments/assets/38d2688c-b349-41da-8903-e7e0a998cf68) + + + +적용 후 로그 + +![Image](https://github.com/user-attachments/assets/b1bec33c-837a-4f39-ad3b-b5d80531d816) + +#### 필터 적용 + + +```java +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +class MDCLoggingFilter implements Filter { + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + final UUID uuid = UUID.randomUUID(); + MDC.put("request_id", uuid.toString()); + filterChain.doFilter(servletRequest, servletResponse); + MDC.clear(); + } +} +``` + +적용 후 로그 +![Image](https://github.com/user-attachments/assets/f689264a-484b-4b9d-83c9-d36de1306268) + +
+ +#### 추가 + +운영환경에서 Nginx를 많이 사용하는데
+nginx 로그, tomcat 로그를 함께 봐야하는 경우가 많다. + +이때 동일한 request_id를 공유하면 로그 파악에 더 도움이 된다고 한다.
+ +![Image](https://github.com/user-attachments/assets/1ca17a53-c823-4df9-b321-a70a83dfe1bc) + + +참고
+ + +https://0soo.tistory.com/246 + +https://mangkyu.tistory.com/266 + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b680e51 --- /dev/null +++ b/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.9' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.ceos21' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // DB + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'com.h2database:h2' + + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e18bc25 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..19b5204 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'spring-boot' diff --git a/src/main/java/com/ceos21/spring_boot/Application.java b/src/main/java/com/ceos21/spring_boot/Application.java new file mode 100644 index 0000000..5731099 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/Application.java @@ -0,0 +1,35 @@ +package com.ceos21.spring_boot; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; + +import java.util.Arrays; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + +// @Bean +// public CommandLineRunner commandLineRunner(ApplicationContext ctx) { +// return args -> { +// +// System.out.println("Let's inspect the beans provided by Spring Boot:"); +// +// // Spring Boot 에서 제공되는 Bean 확인 +// String[] beanNames = ctx.getBeanDefinitionNames(); +// Arrays.sort(beanNames); +// for (String beanName : beanNames) { +// System.out.println(beanName); +// } +// +// }; +// } + +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/RetryAspect.java b/src/main/java/com/ceos21/spring_boot/aop/RetryAspect.java new file mode 100644 index 0000000..3d7aa98 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/RetryAspect.java @@ -0,0 +1,32 @@ +package com.ceos21.spring_boot.aop; + +import com.ceos21.spring_boot.aop.annotation.Retry; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +@Slf4j +@Aspect +public class RetryAspect { + + @Around("@annotation(retry)") + public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable { + log.info("[retry] {} retry: {}", joinPoint.getSignature(), retry); + + int maxValue = retry.value(); + Exception exceptionHolder = null; + + for (int retryCount = 1; retryCount < maxValue; retryCount++) { + try { + log.info("[retry] trying {} times, Max retry: {}", retryCount, maxValue); + return joinPoint.proceed(); + } catch (Exception e) { + exceptionHolder = e; + } + } + throw exceptionHolder; + } + + +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/TestAspect.java b/src/main/java/com/ceos21/spring_boot/aop/TestAspect.java new file mode 100644 index 0000000..f3da8a9 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/TestAspect.java @@ -0,0 +1,36 @@ +package com.ceos21.spring_boot.aop; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Slf4j +@Aspect +@Component +public class TestAspect { + + @Pointcut("execution(* com.ceos21.spring_boot.controller.*.*(..))") + public void controllerAdvice() { + + } + + @Before("controllerAdvice()") + public void requestLogging(JoinPoint joinPoint) { + MDC.put("traceId", UUID.randomUUID().toString()); + + log.info("REQUEST TRACING_ID : {}", MDC.get("traceId")); + } + + @AfterReturning(pointcut = "controllerAdvice()", returning = "result") + public void responseLogging(JoinPoint joinPoint, Object result) { + log.info("RESPONSE TRACING_ID : {}", MDC.get("traceId")); + MDC.clear(); + } +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/TraceAspect.java b/src/main/java/com/ceos21/spring_boot/aop/TraceAspect.java new file mode 100644 index 0000000..e411355 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/TraceAspect.java @@ -0,0 +1,18 @@ +package com.ceos21.spring_boot.aop; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +@Slf4j +@Aspect +public class TraceAspect { + + @Before("@annotation(com.ceos21.spring_boot.aop.annotation.Trace)") + public void doTrace(JoinPoint joinpoint) { + Object[] args = joinpoint.getArgs(); + log.info("[trace] {} args: {}", joinpoint.getSignature(), args); + } + +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/annotation/Retry.java b/src/main/java/com/ceos21/spring_boot/aop/annotation/Retry.java new file mode 100644 index 0000000..ad0ddf2 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/annotation/Retry.java @@ -0,0 +1,14 @@ +package com.ceos21.spring_boot.aop.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Retry { + + int value() default 3; + +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/annotation/Trace.java b/src/main/java/com/ceos21/spring_boot/aop/annotation/Trace.java new file mode 100644 index 0000000..777db75 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/annotation/Trace.java @@ -0,0 +1,12 @@ +package com.ceos21.spring_boot.aop.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Trace { + +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/exam/ExamRepository.java b/src/main/java/com/ceos21/spring_boot/aop/exam/ExamRepository.java new file mode 100644 index 0000000..38331a5 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/exam/ExamRepository.java @@ -0,0 +1,23 @@ +package com.ceos21.spring_boot.aop.exam; + +import com.ceos21.spring_boot.aop.annotation.Retry; +import com.ceos21.spring_boot.aop.annotation.Trace; +import org.springframework.stereotype.Repository; + +@Repository +public class ExamRepository { + + private static int sequence = 0; + + + @Trace + @Retry(value = 4) + public String save(String itemId) { + sequence++; + if (sequence % 5 == 0) { + throw new IllegalArgumentException("Invalid item id: " + itemId); + } + return "ok"; + } + +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/exam/ExamService.java b/src/main/java/com/ceos21/spring_boot/aop/exam/ExamService.java new file mode 100644 index 0000000..2a947de --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/exam/ExamService.java @@ -0,0 +1,17 @@ +package com.ceos21.spring_boot.aop.exam; + +import com.ceos21.spring_boot.aop.annotation.Trace; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ExamService { + + private final ExamRepository examRepository; + + @Trace + public void request(String itemId) { + examRepository.save(itemId); + } +} diff --git a/src/main/java/com/ceos21/spring_boot/aop/filter/MDCLoggingFilter.java b/src/main/java/com/ceos21/spring_boot/aop/filter/MDCLoggingFilter.java new file mode 100644 index 0000000..f9ab3bd --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/aop/filter/MDCLoggingFilter.java @@ -0,0 +1,24 @@ +//package com.ceos21.spring_boot.aop.filter; +// +//import jakarta.servlet.*; +//import org.slf4j.MDC; +//import org.springframework.core.Ordered; +//import org.springframework.core.annotation.Order; +//import org.springframework.stereotype.Component; +// +//import java.io.IOException; +//import java.util.UUID; +// +//@Component +//@Order(Ordered.HIGHEST_PRECEDENCE) +//class MDCLoggingFilter implements Filter { +// +// +// @Override +// public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { +// final UUID uuid = UUID.randomUUID(); +// MDC.put("request_id", uuid.toString()); +// filterChain.doFilter(servletRequest, servletResponse); +// MDC.clear(); +// } +//} diff --git a/src/main/java/com/ceos21/spring_boot/config/DummyTransactionManager.java b/src/main/java/com/ceos21/spring_boot/config/DummyTransactionManager.java new file mode 100644 index 0000000..425e89c --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/config/DummyTransactionManager.java @@ -0,0 +1,55 @@ +package com.ceos21.spring_boot.config; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + +public class DummyTransactionManager implements PlatformTransactionManager { + + private final String name; + + public DummyTransactionManager(String name) { + this.name = name; + } + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + System.out.println("[" + name + "] Transaction started."); + return new DummyTransactionStatus(); + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + System.out.println("[" + name + "] Transaction committed."); + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + System.out.println("[" + name + "] Transaction rolled back."); + } + + private static class DummyTransactionStatus implements TransactionStatus { + @Override public boolean isNewTransaction() { return true; } + @Override public boolean hasSavepoint() { return false; } + @Override public void setRollbackOnly() {} + @Override public boolean isRollbackOnly() { return false; } + @Override public void flush() {} + @Override public boolean isCompleted() { return false; } + + @Override + public Object createSavepoint() throws TransactionException { + return null; + } + + @Override + public void rollbackToSavepoint(Object savepoint) throws TransactionException { + + } + + @Override + public void releaseSavepoint(Object savepoint) throws TransactionException { + + } + } +} diff --git a/src/main/java/com/ceos21/spring_boot/controller/HelloController.java b/src/main/java/com/ceos21/spring_boot/controller/HelloController.java new file mode 100644 index 0000000..449b4e2 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/controller/HelloController.java @@ -0,0 +1,14 @@ +package com.ceos21.spring_boot.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping("/") + public String index() { + return "Greetings from Spring Boot!"; + } + +} diff --git a/src/main/java/com/ceos21/spring_boot/controller/TestController.java b/src/main/java/com/ceos21/spring_boot/controller/TestController.java new file mode 100644 index 0000000..be6d2d9 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/controller/TestController.java @@ -0,0 +1,24 @@ +package com.ceos21.spring_boot.controller; + +import com.ceos21.spring_boot.domain.Test; +import com.ceos21.spring_boot.service.TestService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/tests") +@RequiredArgsConstructor +public class TestController { + + private final TestService testService; + + @GetMapping + public List findAllTests() { + return testService.findAllTest(); + } + +} diff --git a/src/main/java/com/ceos21/spring_boot/domain/Test.java b/src/main/java/com/ceos21/spring_boot/domain/Test.java new file mode 100644 index 0000000..84697d7 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/domain/Test.java @@ -0,0 +1,14 @@ +package com.ceos21.spring_boot.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Getter; + +@Entity +@Getter +public class Test { + + @Id + private Long id; + private String name; +} diff --git a/src/main/java/com/ceos21/spring_boot/repository/TestRepository.java b/src/main/java/com/ceos21/spring_boot/repository/TestRepository.java new file mode 100644 index 0000000..a5b7a4b --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/repository/TestRepository.java @@ -0,0 +1,7 @@ +package com.ceos21.spring_boot.repository; + +import com.ceos21.spring_boot.domain.Test; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TestRepository extends JpaRepository { +} diff --git a/src/main/java/com/ceos21/spring_boot/service/CompareTransactionService.java b/src/main/java/com/ceos21/spring_boot/service/CompareTransactionService.java new file mode 100644 index 0000000..0afe199 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/service/CompareTransactionService.java @@ -0,0 +1,23 @@ +package com.ceos21.spring_boot.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CompareTransactionService { + + private final PlatformTransactionManager transactionManager; + + @Transactional + public void executeTransactional(String managerName, Runnable action) { + TransactionTemplate txTemplate = new TransactionTemplate(transactionManager); + txTemplate.execute(status -> { + action.run(); + return null; + }); + } +} diff --git a/src/main/java/com/ceos21/spring_boot/service/TestService.java b/src/main/java/com/ceos21/spring_boot/service/TestService.java new file mode 100644 index 0000000..9651392 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/service/TestService.java @@ -0,0 +1,21 @@ +package com.ceos21.spring_boot.service; + +import com.ceos21.spring_boot.domain.Test; +import com.ceos21.spring_boot.repository.TestRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TestService { + + private final TestRepository testRepository; + + @Transactional(readOnly = true) + public List findAllTest() { + return testRepository.findAll(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..e02c51d --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: jdbc:h2:tcp://localhost/~/ceos21 + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + +logging: + level: + org.hibernate.SQL: debug \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..c8d34de --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,10 @@ + + + + [%d{yyyy.MM.dd HH:mm:ss.SSS}] - [%-5level] - [%X{request_id}] - [%logger{5}] - %msg%n + + + + + + diff --git a/src/test/java/com/ceos21/spring_boot/ApplicationTests.java b/src/test/java/com/ceos21/spring_boot/ApplicationTests.java new file mode 100644 index 0000000..cfbe7af --- /dev/null +++ b/src/test/java/com/ceos21/spring_boot/ApplicationTests.java @@ -0,0 +1,13 @@ +package com.ceos21.spring_boot; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/ceos21/spring_boot/aop/exam/ExamServiceTest.java b/src/test/java/com/ceos21/spring_boot/aop/exam/ExamServiceTest.java new file mode 100644 index 0000000..8a86029 --- /dev/null +++ b/src/test/java/com/ceos21/spring_boot/aop/exam/ExamServiceTest.java @@ -0,0 +1,28 @@ +package com.ceos21.spring_boot.aop.exam; + +import com.ceos21.spring_boot.aop.RetryAspect; +import com.ceos21.spring_boot.aop.TraceAspect; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +@SpringBootTest +//@Import(TraceAspect.class) +@Import({RetryAspect.class, TraceAspect.class}) +@Slf4j +class ExamServiceTest { + + @Autowired + ExamService examService; + + @Test + void test(){ + for (int i = 1; i < 6; i++) { + log.info("client request: {}", i); + examService.request("item" + i); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/ceos21/spring_boot/controller/HelloControllerTest.java b/src/test/java/com/ceos21/spring_boot/controller/HelloControllerTest.java new file mode 100644 index 0000000..f065f1a --- /dev/null +++ b/src/test/java/com/ceos21/spring_boot/controller/HelloControllerTest.java @@ -0,0 +1,37 @@ +package com.ceos21.spring_boot.controller; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class HelloControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("HelloController 호출 시 Greetings 메세지 출력") + void getHello() throws Exception { + // given + // when + // then + mockMvc.perform(MockMvcRequestBuilders.get("/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("Greetings from Spring Boot!")); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/ceos21/spring_boot/service/CompareTransactionServiceTest.java b/src/test/java/com/ceos21/spring_boot/service/CompareTransactionServiceTest.java new file mode 100644 index 0000000..abed503 --- /dev/null +++ b/src/test/java/com/ceos21/spring_boot/service/CompareTransactionServiceTest.java @@ -0,0 +1,31 @@ +package com.ceos21.spring_boot.service; + +import com.ceos21.spring_boot.config.DummyTransactionManager; +import org.junit.jupiter.api.Test; + + +class CompareTransactionServiceTest { + + @Test + public void testJpaTransactionManager() { + // "JPA" DummyTransactionManager 주입 + DummyTransactionManager jpaTM = new DummyTransactionManager("JPA"); + CompareTransactionService service = new CompareTransactionService(jpaTM); + + System.out.println("== JPA Transaction Manager Test 시작 =="); + service.executeTransactional("JPA", () -> System.out.println("Action executed using JPA TM")); + System.out.println("== JPA Transaction Manager Test 종료 =="); + } + + @Test + public void testJdbcTransactionManager() { + // "JDBC" DummyTransactionManager 주입 + DummyTransactionManager jdbcTM = new DummyTransactionManager("JDBC"); + CompareTransactionService service = new CompareTransactionService(jdbcTM); + + System.out.println("== JDBC Transaction Manager Test 시작 =="); + service.executeTransactional("JDBC", () -> System.out.println("Action executed using JDBC TM")); + System.out.println("== JDBC Transaction Manager Test 종료 =="); + } + +} \ No newline at end of file