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..4c75e42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +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/ + +.env \ No newline at end of file diff --git a/README.md b/README.md index 4162bb5..a0146a1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,461 @@ -# spring-tutorial-24th -CEOS 백엔드 24기 스프링 튜토리얼 +## AOP(Aspect-Oriented Programming) + +관점 지향 프로그래밍. 핵심 비즈니스 로직(**`핵심관심사항`**)과 부가적인 기능(로깅, 보안 등)(**`공통관심사항`**)을 분리하여 모듈화하고, 이를 통해 코드의 재사용성, 유지보수성, 확장성을 높이는 프로그래밍 기법. + +### AOP가 필요한 상황 + +공통 관심 사항: 1000개의 메소드의 호출 시간을 측정한다. +핵심 관심 사항: 각 메소드마다 핵심 비즈니스 로직이 들어있다. + +**문제** + +- 모든 메소드의 시간을 측정하는 로직은 `공통관심사항`이다. +- 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여 유지보수가 어렵다. +- 시간을 측정하는 로직을 별도의 `공통 로직`으로 만들기 어렵다. +- 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다. + **> AOP가 필요하다** + +![](https://velog.velcdn.com/images/kkangmen/post/d2fd0096-556e-4358-a311-f3f9b77ddc0f/image.png) + +### AOP 적용 + +![](https://velog.velcdn.com/images/kkangmen/post/a6855516-bc68-4e46-b0c3-7dd9225ce828/image.png) + +**해결** + +- 회원가입, 회원조회 등 `핵심 관심사항`과 시간을 측정하는 `공통 관심사항`을 분리한다. +- 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.(AOP를 활용하여`@Aspect`) +- 핵심 관심사항을 깔끔하게 유지할 수 있다. +- 원하는 적용대상을 직접 선택할 수 있다.(`@Around`) + +**동작방식** + +![](https://velog.velcdn.com/images/kkangmen/post/e7acd419-2781-47ff-a5e5-054c98a8bf32/image.png) + +- 스프링은 객체를 감싸는 프록시 객체를 생성해서, 공통 로직을 프록시에서 실행하고, 그 다음에 진짜 대상 객체를 호출한다. + + `Client -> Proxy -> Real Object` + + +```java +@Aspect +public class TimeTraceAop { + + @Around("execution(* hello.hello_spring..*(..))") + public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + + System.out.println("START: " + joinPoint.toString()); + + try { + return joinPoint.proceed(); + } finally { + long end = System.currentTimeMillis(); + long timeMs = end - start; + System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms"); + } + } +} +``` + +## PSA (Portable Service Abstraction) + +### 정의 + +- **Portable(이식 가능한)**: 코드를 다른 환경, 다른 기술로 옮겨도 그대로 동작 +- **Service**: 트랜잭션, 캐시, 메일, 메시징 같은 인프라 서비스 +- **Abstraction**: 공통 인터페이스로 감싸기 + +즉, **특정 기술에 종속되지 않도록 서비스를 인터페이스로 추상화해두고, 실제 구현체는 갈아 끼울 수 있게 만든 Spring의 설계원칙**이다. + +구체적 예시로는 `@Transactional`이 있다. Spring은 트랜잭션 방식을 추상화하여 JDBC, JPA, Hibernate 등 다양한 기술 스택에서 일관된 방식으로 동작하게 한다. + +![img.png](img.png) + +### SA (Service Abstraction) + +소프트웨어 개발에서 특정 서비스를 구현할 때, 구현 세부사항을 숨기고 인터페이스만을 제공하는 개념이다. 사용자는 세부 구현을 알 필요 없이, 추상화된 인터페이스를 통해 서비스를 사용할 수 있다. + +**인터페이스** + +```java +public interface PaymentService { + void pay(double amout); +} +``` + +**구현체** + +```java +public class CreditCardPaymentService implements PaymentService { + + @Override + public voic pay(double amount){ + log.info("Paid={} using Credit Card", amount); + } +} + +public class PayPalPaymentService implements PaymentService { + + @Override + public voic pay(double amount){ + log.info("Paid={} using Paypal", amount); + } +} +``` + +**PaymentConfig** + +```java +@Configuration +public class PaymentConfig { + + @Bean + public PaymentService paymentService(){ + return new CreditCardPaymentService(); // 유동적으로 교체 가능 + return new PayPalPaymentService(); + } +} +``` + +PSA는 SA뿐 아니라 동일한 코드를 **`다양한 환경 (로컬, 클라우드, 온프레미스)에서 재사용할 수 있도록`** 설계된 추상화이다. + +## 스프링 어노테이션 + +프로그램 코드에 메타데이터를 주입하여, 컴파일러나 런타임 환경에서 이를 읽고 특정 동작을 수행하도록 지시하는 기능. + +그 자체는 어떠한 실행 로직도 가지지 않는 껍데기에 불과하다. + +### 내부 동작 흐름 + +[컴파일 타임] + +1. `@interface`로 어노테이션 정의 +2. 타깃(`UserService`)에 선언 + 속성값(`role=ADMIN`) 지정 + +[런타임] + +1. `UserService` 빈 생성/초기화 +2. `AbstractAutoProxyCreator`가 UserService의 메서드를 리플렉션으로 순회하며 포인트컷(`@RoleCheck`) 매칭 → 어노테이션 프록시 생성 +3. 매칭되면 UserService AOP 프록시 생성 + +[호출 시점] + +1. `deleteUser()` 호출 → AOP 프록시가 어드바이스 체인 실행 +2. 어드바이스가 어노테이션 프록시에서 `role()` 조회 + +**리플렉션** + +자바 프로그램이 실행 중에 `자기 자신의 내부 구조를 엑스레이처럼 들여다보고 조작할 수 있게 해주는 기능`. + +### 예시 + +```java +// 어노테이션 정의 +@Retention(RetentionPolicy.RUNTIME) // 언제까지 살아남나 -> RUNTIME = 리플렉션 +@Target(ElementType.METHOD) // 어디에 붙일 수 있나 -> METHOD는 클래스 내부 메서드 +public @interface RoleCheck { + String role() default "USER"; +} + +// 어노테이션 사용 +public class UserService { + + @RoleCheck(role = "ADMIN") + public void deleteUser(){ + log.info("유저 삭제 로직"); + } +} + +// 리플렉션을 이용한 어노테이션 분석 및 동작 +public class AnnotationTest { + public static void main(String[] args) throws Exception { + + // UserService 클래스의 deleteUser 메서드 정보를 리플렉션으로 가져옴 + Method method = UserService.class.getMethod("deleteUser"); + + // 메서드에 선언된 @RoleCheck 어노테이션 객체 추출 + RoleCheck annotation = method.getAnnotation(RoleCheck.class); + + if (annotation != null) { + // 프록시 객체에 담긴 값을 읽어옴 + System.out.println("설정된 권한: " + annotation.role()); // ADMIN + + // 어노테이션 객체의 실제 클래스 타입 확인 + System.out.println("어노테이션 구현체 타입: " + annotation.getClass().getName()); // com.sun.proxy.$Proxy1 + } + } +} +``` + +### 어노테이션 기반 Bean 등록 과정 + +1. **설정 읽기**: `@Configuration`이 붙은 설정 클래스나 `@SpringBootApplication`을 로드하여 애플리케이션의 시작점 인식 +2. **컴포넌트 스캔**: 지정된 기준 패키지부터 하위 패키지까지 탐색하여 빈으로 등록할 대상 클래스들을 찾는다. +3. **BeanDefinition 생성**: 찾아낸 클래스들의 메타데이터(클래스 이름, 스코프, 의존성 등)를 담은 `BeanDefinition` 설계도 객체를 생성해 레지스트리에 등록 +4. **인스턴스화 및 DI**: `BeanDefinition`을 바탕으로 실제 객체를 메모리에 생성하고, `@Autowired` 등을 확인하여 의존성을 주입 +5. **초기화**: `@PostConstruct`가 선언된 메서등 등 초기화 콜백을 실행하여 빈을 완성 상태로 만든다. + +### @ComponentScan의 컴포넌트 탐색 과정 + +**시작점** + +```java +@SpringBootApplication // <- 를 상속한 @ComponentScan부터 탐색 시작 +public class Application {} +``` + +1. 패키지명을 파일 경로 패턴으로 변환 + + `com.example.demo → classpath*:com/example/demo/**/*.class` + + `PathMatchingResourcePatternResolver`가 이 패턴으로 클래스패스 전체를 탐색해 `.class` 파일 목록을 모은다. + +2. 바이트코드를 읽는다. + + ASM 라이브러리로 `.class` 파일 바이트를 직접 파싱한다. + +3. 필터로 걸러낸다. + + `@Component`가 붙었는지 본다. + +4. 이름을 붙여 등록 + + `AnnotationBeanNameGenerator`가 이름을 정한다. 그리고 레지스트리에 등록. + + +### 여러 구현체가 있는 인터페이스의 의존성 주입 + +하나의 인터페이스를 구현한 클래스가 여러 개일 때, `@Autowired`만 사용하면 스프링은 어떤 빈을 주입해야 할지 몰라 `NoUniqueBeanDefinitaionException` 에러를 발생시킨다. 이를 해결하는 방법 3가지 + +| 해결 방법 | 설명 | 활용 시기 | +| --- | --- | --- | +| @Primary | 여러 구현체 중 주입될 우선순위를 가진 빈을 지정 | 메인으로 사용하는 기본 구현체가 명확할 때 | +| @Qualifier | 주입받는 곳에 `Qualifier("빈이름")`을 적어 특정 구현체를 명시 | 특정 로직에서 명시적으로 다른 서브 구현체를 사용해야 할 때 | +| Collection 주입 | `Map` 형태로 모든 구현테를 주입받는다. | 런타임 조건에 따라 사용할 구현체를 동적으로 선택 (`전략 패턴`)해야 할 때 | + +**@Primary** + +```java +@Component +@Primary +public class RateDiscountPolicy implements DiscountPolicy {} +``` + +**@Qualifier** + +```java +@Component +@Qualifier("mainDiscountPolicy") +public class RateDiscountPolicy implements DiscountPolicy{} + +// @Qualifier가 붙은 RateDiscountPolicy로 DI +@Autowired +public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) +``` + +**Collection 주입** + +```java +@Service +@RequiredArgsConstructor +@Slf4j +public class BacktestService { + + private final Map strategyMap; + + /*** + * 전달받은 종목코드, 투자성향, 시작일, 종료일을 기준으로 백테스트 전략을 수행한다. + * @param stockCode 종목 코드 + * @param investType 투자 성향 (1~5) + * @param startDate 시작일 + * @param endDate 종료일 + * @return BacktestResDto.QuantScoringResponse + */ + @Transactional + public BacktestResDto.QuantScoringResponse runStrategy( + String stockCode, int investType, LocalDate startDate, LocalDate endDate){ + + // 생략... + + // 전략 구축 + Strategy strategy; + try { + strategy = strategyMap.get(strategyName).strategy(series); + } catch (NullPointerException e){ + throw new BacktestException(BacktestErrorCode.STRATEGY_NOT_FOUND); + } +``` + +## Spring MVC + +### MVC + +![img_1.png](img_1.png) + +Model View Controller의 약자로 애플리케이션을 세가지의 역할로 구분한 개발 방법론. 그림처럼 사용자가 `컨트롤러`를 조작하면 `컨트롤러`는 `모델`을 통해서 데이터를 가져오고 그 정보를 바탕으로 시각적인 표현을 담당하는 `뷰`를 제어해서 사용자에게 전달한다. + +### 웹으로 오면서 (Spring MVC) + +HTTP는 요청-응답이 끝나면 연결이 끊긴다. 그래서 `모델`이 `뷰`에게 알려줄 방법이 없다. + +그래서 웹에서는 `뷰`가 `모델`을 관찰하는 게 아니라, `컨트롤러`가 `모델`을 만들어 `뷰`에 밀어 넣는다. + +→ MVC는 **역할을 셋으로 나눠라**는 원칙 + +→ Spring MVC는 **그 원칙을 서블릿 위에서 컨트롤러 패턴으로 구현한 구체적인 프레임워크** + +## Tomcat과 WAS, 서블릿과 웹 요청 처리 + +| | 웹 서버 | WAS (웹 애플리케이션 서버) | +| --- | --- | --- | +| 대표 | Nginx | Tomcat | +| 처리 대상 | 정적 리소스 | 동적 콘텐츠 | +| 구성 | HTTP 처리 | HTTP 처리 + 애플리케이션 컨테이너 | + +![img_2.png](img_2.png) + +- 서블릿 컨테이너 (Tomcat) + - 역할: 서블릿들을 관리. 요청에 따라 동적으로 응답 생성 +- 스프링 DI 컨테이너 + - 역할: 빈 생명 주기 관리, 싱글톤 유지 +- 그림에서 어느 부분에 DI 컨테이너가 개입? + - 스프링은 `DispatcherServlet`이라는 단 1개의 거대한 대표 서블릿만 만들어 두고 모든 요청을 다 받는다. (`즉, 그림에서 파란색 Servlet이 하나만 존재`) + 1. `init()` 호출 시점 (스프링 컨테이너의 탄생) + - Tomcat이 켜지고 `DispatcherServlet`을 생성하면서 `init()` 메서드를 호출. + - 바로 이때 `DispatcherServlet`은 내부에 스프링 컨테이너(`ApplicationContext`)를 생성. + - 그리고 작성한 `@Controller`, `@Service`, `@Repository`같은 클래스들을 Bean으로 만들어서 스프링 컨테이너라는 보관함에 싹 올려두고 의존성 주입을 완료함. + 2. `service()`호출 및 스레드 동작 시점 (스프링 컨테이너의 활약) + - 클라이언트의 요청이 오면 Tomcat이 스레드를 하나 뽑아서 `DispatcherServlet`의 `service()` 메서드를 실행. (이때부터 Tomcat의 역할은 끝이고, 스프링의 주도권 시작.) + - `DispatcherServlet`은 자신이 품고 있는 스프링(`DI`) 컨테이너에게 물어봄. + + “지금 `/api/items`라는 URL로 요청이 왔는데, 네가 관리하는 Bean 중에 이거 처리할 `@Controller` 누구야?” + + - 스프링 컨테이너가 알맞은 컨트롤러 Bean을 찾아주면, 그 컨트롤러의 로직을 실행하고 결과를 받아옴. + 3. 종료 시점 (`distroy()`) + - Tomcat 서버가 종료될 때 `DispatcherServlet`의 `destroy()`가 호출되며, 내부에 있던 스프링(`DI`) 컨테이너도 함께 파괴되면서 관리하면 Bean들도 소멸. + +## DispatcherServlet 동작 원리 + +디스패처 서블릿: 모든 요청을 가장 먼저 받는 `프론트 컨트롤러`. 요청을 직접 처리하지 않고 적절한 컨트롤러를 찾아 위임하는 `중앙 집중형 서블릿 구현체`이다. + +1. **서블릿 요청을 HTTP로 캐스팅 → HTTP 메서드에 따라 분기** + + 필터들을 통과한 요청이 가장 먼저 닿는 곳이 `HttpServlet`의 `service()`이다. `ServletRequest/ServletResponse`를 HTTP용 타입으로 형변환하고, 실패하면 HTTP 요청이 아니라는 뜻이므로 예외를 던진다. + + ```java + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String method = req.getMethod(); + + if (method.equals(METHOD_GET)) { + long lastModified = getLastModified(req); + if (lastModified == -1) { + // servlet doesn't support if-modified-since, no reason + // to go through further expensive logic + doGet(req, resp); + } else { + long ifModifiedSince; + try { + ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); + } catch (IllegalArgumentException iae) { + // Invalid date header - proceed as if none was set + ifModifiedSince = -1; + } + if (ifModifiedSince < (lastModified / 1000 * 1000)) { + // If the servlet mod time is later, call doGet() + // Round down to the nearest second for a proper compare + // A ifModifiedSince of -1 will always be less + maybeSetLastModified(resp, lastModified); + doGet(req, resp); + } else { + resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + } + + } else if (method.equals(METHOD_HEAD)) { + long lastModified = getLastModified(req); + maybeSetLastModified(resp, lastModified); + doHead(req, resp); + + } else if (method.equals(METHOD_POST)) { + doPost(req, resp); + + } else if (method.equals(METHOD_PUT)) { + doPut(req, resp); + + } else if (method.equals(METHOD_DELETE)) { + doDelete(req, resp); + + } else if (method.equals(METHOD_OPTIONS)) { + doOptions(req, resp); + + } else if (method.equals(METHOD_TRACE)) { + doTrace(req, resp); + + } else { + // + // Note that this means NO servlet supports whatever + // method was requested, anywhere on this server. + // + + String errMsg = lStrings.getString("http.method_not_implemented"); + Object[] errArgs = new Object[1]; + errArgs[0] = method; + errMsg = MessageFormat.format(errMsg, errArgs); + + resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); + } + } + ``` + +2. **공통 전처리** + + `doGet()`, `doPost()` 등 `doX` 메서드들은 자식인 FrameworkServlet에 오버라이드되어 있고, 모두 `processRequest()`를 부른다. (템플릿 메서드 패턴) + +3. **doDispatch()** + + ![img_3.png](img_3.png) + + ```java + protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + ... + processedRequest = checkMultipart(request); + + // 1. 요청에 매핑되는 HandlerExecutionChain 조회 + mappedHandler = getHandler(processedRequest); + if (mappedHandler == null) { + noHandlerFound(processedRequest, response); + return; + } + + // 2. 요청을 처리할 HandlerAdapter 조회 + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + // 3. 어댑터를 통해 컨트롤러 호출 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + ... + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + } + ``` + + 1. **HandlerExcecutionChain 조회** + + `getHandler()`는 등록된 `HandlerMapping` 목록을 순회하며 처리 가능한 것을 찾는다. + + **HandlerMapping (핸들러 매핑)** + + - `0 = RequestMappingHandlerMapping`: 어노테이션 기반의 컨트롤러인 @RequestMapping에서 사용 + - `1 = BeanNameUrlHandlerMapping`: 스프링 빈의 이름으로 핸들러를 찾는다. + 2. **HandlerAdapter 조회** + + 디스패서 서블릿은 찾아낸 핸들러를 직접 실행하지 않는다. `HandlerAdapter`라는 어댑터 인터페이스를 통해 실행한다. + + **HandlerAdapter (핸들러 어댑터)** + + - `0 = RequestMappingHandlerAdapter`: 어노테이션 기반의 컨트롤러인 @RequestMapping에서 사용 + - `1 = HttpRequestHandlerAdapter`: HttpRequestHandler 처리 + - `2 = SimpleControllerHandlerAdapter`: Controller 인터페이스 처리 + 3. 컨트롤러 호출 + + 내가 작성한 로직 실행 \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..570a8d4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '4.1.1' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.ceos24' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-webmvc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + runtimeOnly 'com.mysql:mysql-connector-j' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation('org.springframework.boot:spring-boot-starter-test') +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 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..ad7845b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 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 +# + +############################################################################## +# +# gradlew 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 gradlew +# +# 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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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\n' "$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 + + + +# 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" ) + + 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, 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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..a51ec4f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@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 gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +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 + +"%COMSPEC%" /c exit 1 + +: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 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/img.png b/img.png new file mode 100644 index 0000000..f8a3464 Binary files /dev/null and b/img.png differ diff --git a/img_1.png b/img_1.png new file mode 100644 index 0000000..35d0110 Binary files /dev/null and b/img_1.png differ diff --git a/img_2.png b/img_2.png new file mode 100644 index 0000000..1d47dda Binary files /dev/null and b/img_2.png differ diff --git a/img_3.png b/img_3.png new file mode 100644 index 0000000..c70587e Binary files /dev/null and b/img_3.png differ 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/ceos24/spring_boot/Application.java b/src/main/java/com/ceos24/spring_boot/Application.java new file mode 100644 index 0000000..f161426 --- /dev/null +++ b/src/main/java/com/ceos24/spring_boot/Application.java @@ -0,0 +1,33 @@ +package com.ceos24.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/ceos24/spring_boot/HelloController.java b/src/main/java/com/ceos24/spring_boot/HelloController.java new file mode 100644 index 0000000..204e757 --- /dev/null +++ b/src/main/java/com/ceos24/spring_boot/HelloController.java @@ -0,0 +1,13 @@ +package com.ceos24.spring_boot; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping("/") + public String hello(){ + return "Hello, Spring Boot!"; + } +} diff --git a/src/main/java/com/ceos24/spring_boot/test/Test.java b/src/main/java/com/ceos24/spring_boot/test/Test.java new file mode 100644 index 0000000..51c48dc --- /dev/null +++ b/src/main/java/com/ceos24/spring_boot/test/Test.java @@ -0,0 +1,15 @@ +package com.ceos24.spring_boot.test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Data; + +@Data +@Entity +public class Test { + + @Id + private Long id; + + private String name; +} diff --git a/src/main/java/com/ceos24/spring_boot/test/TestController.java b/src/main/java/com/ceos24/spring_boot/test/TestController.java new file mode 100644 index 0000000..1bc6597 --- /dev/null +++ b/src/main/java/com/ceos24/spring_boot/test/TestController.java @@ -0,0 +1,21 @@ +package com.ceos24.spring_boot.test; + +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 +@RequiredArgsConstructor +@RequestMapping("/tests") +public class TestController { + + private final TestService testService; + + @GetMapping + public List findAllTest(){ + return testService.findAllTests(); + } +} diff --git a/src/main/java/com/ceos24/spring_boot/test/TestRepository.java b/src/main/java/com/ceos24/spring_boot/test/TestRepository.java new file mode 100644 index 0000000..30ce519 --- /dev/null +++ b/src/main/java/com/ceos24/spring_boot/test/TestRepository.java @@ -0,0 +1,6 @@ +package com.ceos24.spring_boot.test; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TestRepository extends JpaRepository { +} diff --git a/src/main/java/com/ceos24/spring_boot/test/TestService.java b/src/main/java/com/ceos24/spring_boot/test/TestService.java new file mode 100644 index 0000000..ddc44ea --- /dev/null +++ b/src/main/java/com/ceos24/spring_boot/test/TestService.java @@ -0,0 +1,19 @@ +package com.ceos24.spring_boot.test; + +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 findAllTests(){ + return testRepository.findAll(); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..4fad115 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/test_db?allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=UTF-8 + username: root + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + +logging: + level: + org.hibernate.SQL: debug \ No newline at end of file diff --git a/src/test/java/com/ceos24/spring_boot/ApplicationTests.java b/src/test/java/com/ceos24/spring_boot/ApplicationTests.java new file mode 100644 index 0000000..26bf847 --- /dev/null +++ b/src/test/java/com/ceos24/spring_boot/ApplicationTests.java @@ -0,0 +1,13 @@ +package com.ceos24.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/ceos24/spring_boot/HelloControllerTest.java b/src/test/java/com/ceos24/spring_boot/HelloControllerTest.java new file mode 100644 index 0000000..0075088 --- /dev/null +++ b/src/test/java/com/ceos24/spring_boot/HelloControllerTest.java @@ -0,0 +1,32 @@ +package com.ceos24.spring_boot; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.web.servlet.MockMvc; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@AutoConfigureMockMvc +class HelloControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + @DisplayName("HellowController의 hello() 메서드 테스트") + public void getHello() throws Exception { + mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(result -> { + String response = result.getResponse().getContentAsString(); + assert response.equals("Hello, Spring Boot!"); + }); + } +} \ No newline at end of file