diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d70d5a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +*# +*.iml +*.ipr +*.iws +*.jar +*.sw? +*~ +.#* +.*.md.html +.DS_Store +.attach_pid* +.classpath +.factorypath +.gradle +.metadata +.project +.recommenders +.settings +.springBeans +.vscode +/code +MANIFEST.MF +_site/ +activemq-data +bin +build +!/**/src/**/bin +!/**/src/**/build +build.log +dependency-reduced-pom.xml +dump.rdb +interpolated*.xml +lib/ +manifest.yml +out +overridedb.* +target +.flattened-pom.xml +secrets.yml +.gradletasknamecache +.sts4-cache + +.idea \ No newline at end of file diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000..e69de29 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b1ac1a7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.3' + 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' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.projectlombok:lombok:1.18.30' + implementation 'org.projectlombok:lombok:1.18.30' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + +} + +tasks.named('test') { + useJUnitPlatform() +} 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 100644 index 0000000..e69de29 diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e69de29 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/spring-tutorial-21st b/spring-tutorial-21st new file mode 160000 index 0000000..749e652 --- /dev/null +++ b/spring-tutorial-21st @@ -0,0 +1 @@ +Subproject commit 749e652fb1b931d95220118e8df98f8c189a7a9d 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..087d948 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/Application.java @@ -0,0 +1,32 @@ +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/controller/HelloController.java b/src/main/java/com/ceos21/spring_boot/controller/HelloController.java new file mode 100644 index 0000000..1f94fe5 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/controller/HelloController.java @@ -0,0 +1,13 @@ +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 "Greeting 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..47142ed --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/controller/TestController.java @@ -0,0 +1,22 @@ +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 +@RequiredArgsConstructor +@RequestMapping(value = "/tests") +public class TestController { + + private final TestService testService; + @GetMapping + public List findAllTests() { + return testService.findAllTests(); + } +} 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..05fd8fd --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/domain/Test.java @@ -0,0 +1,13 @@ +package com.ceos21.spring_boot.domain; + +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/ceos21/spring_boot/domain/TestRepository.java b/src/main/java/com/ceos21/spring_boot/domain/TestRepository.java new file mode 100644 index 0000000..2600710 --- /dev/null +++ b/src/main/java/com/ceos21/spring_boot/domain/TestRepository.java @@ -0,0 +1,7 @@ +package com.ceos21.spring_boot.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TestRepository extends JpaRepository { + +} 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..400a68f --- /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.domain.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 findAllTests() { + return testRepository.findAll(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..73e27df --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:h2:tcp://localhost/~/ceos21 + username: sa + password: 1234 + driver-class-name: org.h2.Driver + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + +logging: + level: + org.hibernate.sql : debug + +server: + port: 8081 \ No newline at end of file diff --git a/src/main/spring-tutorial-21st/README.md b/src/main/spring-tutorial-21st/README.md new file mode 100644 index 0000000..2061e32 --- /dev/null +++ b/src/main/spring-tutorial-21st/README.md @@ -0,0 +1,169 @@ +## [스프링이 지원하는 기술들] + +### 1. IoC (Inversion of Control) + +- 객체의 생명 주기와 의존성 관리를 스프링 컨테이너 대신 수행하는 기술 +- 직접 객체를 생성하는 것이 아니라, 객체의 제어권을 스프링에 넘겨 코드 간의 결합도를 낮추고 유지 보수를 용이하게 한다. + +### 2. DI (Dependency Injection) + +- IoC의 구체적인 구현 방식으로, 객체 간의 의존성을 외부에서 주입 받는 기술 +- DI를 사용하면, 의존 객체를 코드 내에서 직접 생성하는 대신, 설정을 통해 외부에서 주입받게 된다. +- @Autowired + +### 3. AOP (Aspect-Oriented Programming) + +- 로깅, 트렌젝션, 보안 등과 같은 부가 기능을 비즈니스 로직과 분리하여 용이하게 한다. + +#### 정리 + +- 클래스는 스프링 컨테이너 위에서 오브젝트로 만들어져 동작한다. +- 스프링의 프로그래밍 모델에 따라 작성한다. +- 엔터프라이즈 기술 활용 시 스프링 API 와 서비스를 활용한다. + +## [Spring Bean 이 무엇이고, Bean 의 라이프사이클은 어떻게 되는지 조사해요] + +### 1. Spring Bean 이란? + +- 스프링 IoC 컨테이너가 생성 및 관리하는 자바 객체 +- 과거에는 개발자가 `new`연산자로 객체를 생성하고 생명 주기를 관리 +- IoC 기술을 통해 객체 생성과 관리를 스프링이 대신 +- **스프링 컨테이너에서 관리되는 객체** = **Bean** + +### 2. Bean 등록 방법 + +- @Component, @Controller, @RestCotroller, @Service, @Repository 등이 존재 (클래스 단위로 등록) +- @Bean (메소드 단위로 등록) + +### 3. 라이프 사이클 + +- 스프링 IoC 컨테이너 생성 +- 스프링 Bean 생성 +- 의존 관계 주입 +- 초기화 콜백 메소드 호출 (빈의 초기화 작업) +- 로직 수행 및 빈 사용 +- 소멸 전 콜백 메소드 호출 +- 스프링 종료 + +## [스프링 어노테이션을 심층 분석해요] + +### 1. 어노테이션이란? + +- 자바 소스 코드에 추가하는 메타 데이터 +- 실제 실행에는 영향을 주지 않으나 코드의 동작 방식을 설정하거나 특정 동작을 수행하게 만듦 + +### 2. 빈 등록 시 일어나는 과정 분석 + +- Component Scan을 통해 어노테이션이 붙은 클래스를 탐색 +- 빈 정보를 등록 +- 이를 바탕으로 빈 객체를 생성하고, 생성된 빈 사이의 의존성 주입 +- 빈 객체를 IoC 컨테이너에서 관리 + +### 3. @ComponentScan + +- 위 어노테이션을 통해 class path를 탐색하여 자동으로 빈 등록 +- 스프링은 Application 실행 시 @ComponentScan을 기반으로 지정된 패키지 내에서 어노테이션이 부착된 클래스를 탐색 (명시하지 않으면 @ComponentScan을 선언한 클래스의 패키지가 기준) + +## **[단위 테스트와 통합 테스트 탐구]** + +### 1. 단위 테스트 + +- 하나의 코드 단위 (메서드 또는 클래스) 가 독립적으로 정상 동작하는지 확인하는 테스트 +- 테스트 수행 시간이 빠르고 자주 수행 가능 → 최소한의 요소만 가져와서 수행 가능하다. +- 코드 내부 로직에 집중 +- Mock 객체를 사용하여서 외부 요소를 격리 가능!! + +```jsx +@ExtendWith(MockitoExtension.class) +public class AccountServiceTest extends DummyObject { + @InjectMocks // 모든 Mock 들이 InjectionMock 로 주입 + private AccountService accountService; + + @Mock + private UserRepository userRepository; + + @Mock + private AccountRepository accountRepository; + + @Spy // 진짜 객체를 InjectMocks 에 주입 + private ObjectMapper om; + + @Test + public void 계좌등록_test() throws Exception { + // given + Long userId = 1L; + + AccountReqDto.AccountSaveReqDto accountSaveReqDto = new AccountReqDto.AccountSaveReqDto(); + accountSaveReqDto.setNumber(1111L); + accountSaveReqDto.setPassword(1234L); + + // stub 1 + User ssar = newMockUser(userId, "ssar", "쌀"); + when(userRepository.findById(any())).thenReturn(Optional.of(ssar)); + + // stub 2 + when(accountRepository.findByNumber(any())).thenReturn(Optional.empty()); + + // stub 3 + Account ssarAccount = newMockAccount(1L,1111L, 1000L, ssar); + when(accountRepository.save(any())).thenReturn(ssarAccount); + + // when + AccountResDto.AccountSaveResDto accountSaveResDto = accountService.계좌등록(accountSaveReqDto, userId); + String responseBody = om.writeValueAsString(accountSaveResDto); + System.out.println("테스트: " + responseBody); + + // then + assertThat(accountSaveResDto.getNumber()).isEqualTo(1111L); + + } + +} +``` + +- Mockito란? + - 자바 오픈 소스 테스트 프레임 워크 +- @Mock + - 특정 개체를 test 내에서 어노테이션을 통해 mock 객체로 바인딩 한다. (가짜 객체) + - mock은 개발자가 지저한 stub 환경 외의 기능은 동작하지 않는다. + - stub : mock 객체 생성의 동작을 지정하는 것, 테스트의 결과를 설정, 특정 매개변수를 받았을 때 특정 값을 return 또는 예외를 던질 수 있음 +- @Spy + - mock 객체는 개발자가 지정한 Stub 외의 기능은 동작하지 않는데, 만약 stub를 제외한 나머지를 그대로 사용하고 싶으면 ? → spy 사용 + - 실제 객체를 생성하고 메서드를 감시한다. +- @InjectMocks + - @Mock, @Spy 로 지정된 mock 객체들 중 필요한 객체를 주입시킨다. + +### 2. 통합 테스트 + +- 통합 테스트 (integration Test) + - 모듈 또는 두 개 이상의 클래스가 함께 상호 작용하여 정상적으로 동작하는지 검증 + - 즉, 서로 다른 클래스가 함께 있을 때 문제를 발견하기 위함 + - 테스트 속도는 더 느림 + + ```java + // UserService와 UserRepository를 함께 테스트 + @SpringBootTest + class UserIntegrationTest { + + @Autowired + // 차이점 + UserService userService; + + @Test + void testUserRegistration() { + User user = new User("John"); + userService.registerUser(user); + User result = userService.findUser("John"); + + assertNotNull(result); + assertEquals("John", result.getName()); + } + } + + ``` + +- @Mockbean, @MockSpy + - mock과 비슷하게 spring context에 mock으로 변한 bean에 등록되게 된다. + - 대신 @InjectMocks가 아닌 @Autowired를 사용 + - 스프링 컨텍스트에 존재하는 기존 빈을 대체하여 등록 + - \ No newline at end of file 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..57f67c4 --- /dev/null +++ b/src/test/java/com/ceos21/spring_boot/ApplicationTests.java @@ -0,0 +1,14 @@ +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/controller/HelloControllerTest.java b/src/test/java/com/ceos21/spring_boot/controller/HelloControllerTest.java new file mode 100644 index 0000000..c767897 --- /dev/null +++ b/src/test/java/com/ceos21/spring_boot/controller/HelloControllerTest.java @@ -0,0 +1,52 @@ +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.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.assertj.core.api.Assertions.*; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class HelloControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + @DisplayName("Hello Controller success test") + public void getHello_success_test() throws Exception { + // given + String expectedResult = "Greeting from Spring boot"; + + // when + ResultActions result = mvc.perform(get("/").contentType(MediaType.APPLICATION_JSON)); + String responseBody = result.andReturn().getResponse().getContentAsString(); + System.out.println("성공 테스트 : " + responseBody); + + // then + assertThat(responseBody).isEqualTo(expectedResult); + } + + @Test + @DisplayName("Hello Controller fail Test : URL Not FOUND") + public void getHello_fail_test() throws Exception { + // given + String invalidUrl = "/invalid"; + + //when + ResultActions result = mvc.perform(MockMvcRequestBuilders.get(invalidUrl).accept(MediaType.APPLICATION_JSON)); + + // then + result.andExpect(status().isNotFound()); + } +}