Skip to content

[21기_한혜수] spring tutorial 미션 제출합니다. #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: hyesuhan
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Empty file added HELP.md
Empty file.
36 changes: 36 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -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()
}
7 changes: 7 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Empty file added gradlew
Empty file.
Empty file added gradlew.bat
Empty file.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'spring-boot'
1 change: 1 addition & 0 deletions spring-tutorial-21st
Submodule spring-tutorial-21st added at 749e65
32 changes: 32 additions & 0 deletions src/main/java/com/ceos21/spring_boot/Application.java
Original file line number Diff line number Diff line change
@@ -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);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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";
}

}
Original file line number Diff line number Diff line change
@@ -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<Test> findAllTests() {
return testService.findAllTests();
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/ceos21/spring_boot/domain/Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ceos21.spring_boot.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Data;

@Data
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaTa 어노테이션이 자동으로 생성하는 메서드에 뭐뭐가 있는지 알아봐도 좋을 것 같습니다! 평소에 이 어노테이션을 쓸 일도 볼 일도 별로 없으니.. 이번 기회에 ..!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaTa 는 롬복에서 지원하는 !!(@Setter, @Getter 과 같은) 어노테이션인데, 일반적으로 DTO 같은 모델에서 많이 사용합니다. 찾아보니 @DaTa 어노테이션이 @Getter, @Setter, @tostring 등을 합쳐 놓은 거라네요!! 저... 처음 알았어요.. 한번쯤은 @DaTa@Getter 을 같이 쓴 적도 있는 것 같고, @DaTa 만 사용할 때는 GPT 이용해서 만든거여서 그냥 데이터여서 그렇구나~ 하고 넘어갔는데 이런 이유가 있었네요!!! 평소에 @Setter을 안 사용하려고 해서 @DaTa가 없는 거였어요!!!! 기본적인데 뭔가 까먹고 당연하게 사용하고 있었네용..

@Entity
public class Test {
@Id
private Long id;
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ceos21.spring_boot.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface TestRepository extends JpaRepository<Test, Long> {

}
21 changes: 21 additions & 0 deletions src/main/java/com/ceos21/spring_boot/service/TestService.java
Original file line number Diff line number Diff line change
@@ -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<Test> findAllTests() {
return testRepository.findAll();
}
}
19 changes: 19 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/ceos21
username: sa
password: 1234
driver-class-name: org.h2.Driver
Comment on lines +1 to +6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

환경변수 처리해서 gitignore 사용하면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 복사해서 .gitignore을 안 해놨네요! 다음 과제부터는 ignore 해 놓겠습니다 XD

jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true

logging:
level:
org.hibernate.sql : debug

server:
port: 8081
169 changes: 169 additions & 0 deletions src/main/spring-tutorial-21st/README.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단위테스트랑 통합테스트를 예시코드와 함께 자세히 써주셔서 좋았어요! 실제로 다른 프로젝트를 하면서 썼던 테스트코드같은데 테스트 코드 작성하는 습관 멋집니당....

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저거는 그냥 공부하면서 쓴 것 뿐... 저만 그런지는 모르겠는데 프로젝트에서 테스트를 하려는 습관이 어렵더라고요 . . .(사실 impl 도 하기 시간이 빡세서 ㅠ)

Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
## [스프링이 지원하는 기술들]

### 1. IoC (Inversion of Control)

- 객체의 생명 주기와 의존성 관리를 스프링 컨테이너 대신 수행하는 기술
- 직접 객체를 생성하는 것이 아니라, 객체의 제어권을 스프링에 넘겨 코드 간의 결합도를 낮추고 유지 보수를 용이하게 한다.

### 2. DI (Dependency Injection)

- IoC의 구체적인 구현 방식으로, 객체 간의 의존성을 외부에서 주입 받는 기술
- DI를 사용하면, 의존 객체를 코드 내에서 직접 생성하는 대신, 설정을 통해 외부에서 주입받게 된다.
- @Autowired
Comment on lines +8 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수동 주입 @configuration -> @bean에 대해서도 찾아보면 좋을 것 같아요~!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다!!! 공부해서 정리 해 볼게요 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수동 주입의 경우 @Configuration-@Bean 을 사용하고 자동 주입은 @Component-@Autowired 를 사용한다고 하네요.
기본적으로 자동 주입이 일반적인데, (@controller, @service 와 같은 @component 어노테이션이 비즈니스 로직적으로 이해하기 쉬워요) 다만 기술적인 문제나 공통 관심사를 처리할 때는 수동 빈으로 등록해서 설정 정보가 명확히 드러나는 경우가 더 좋다고 하네요!


### 3. AOP (Aspect-Oriented Programming)

- 로깅, 트렌젝션, 보안 등과 같은 부가 기능을 비즈니스 로직과 분리하여 용이하게 한다.

#### 정리

- 클래스는 스프링 컨테이너 위에서 오브젝트로 만들어져 동작한다.
- 스프링의 프로그래밍 모델에 따라 작성한다.
- 엔터프라이즈 기술 활용 시 스프링 API 와 서비스를 활용한다.

## [Spring Bean 이 무엇이고, Bean 의 라이프사이클은 어떻게 되는지 조사해요]

### 1. Spring Bean 이란?

- 스프링 IoC 컨테이너가 생성 및 관리하는 자바 객체
- 과거에는 개발자가 `new`연산자로 객체를 생성하고 생명 주기를 관리
- IoC 기술을 통해 객체 생성과 관리를 스프링이 대신
- **스프링 컨테이너에서 관리되는 객체** = **Bean**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정리👍입니다


### 2. Bean 등록 방법

- @Component, @Controller, @RestCotroller, @Service, @Repository 등이 존재 (클래스 단위로 등록)
- @Bean (메소드 단위로 등록)

### 3. 라이프 사이클

- 스프링 IoC 컨테이너 생성
- 스프링 Bean 생성
- 의존 관계 주입
- 초기화 콜백 메소드 호출 (빈의 초기화 작업)
- 로직 수행 및 빈 사용
- 소멸 전 콜백 메소드 호출
- 스프링 종료

## [스프링 어노테이션을 심층 분석해요]

### 1. 어노테이션이란?

- 자바 소스 코드에 추가하는 메타 데이터
- 실제 실행에는 영향을 주지 않으나 코드의 동작 방식을 설정하거나 특정 동작을 수행하게 만듦

### 2. 빈 등록 시 일어나는 과정 분석

- Component Scan을 통해 어노테이션이 붙은 클래스를 탐색
- 빈 정보를 등록
- 이를 바탕으로 빈 객체를 생성하고, 생성된 빈 사이의 의존성 주입
- 빈 객체를 IoC 컨테이너에서 관리

### 3. @ComponentScan

- 위 어노테이션을 통해 class path를 탐색하여 자동으로 빈 등록
- 스프링은 Application 실행 시 @ComponentScan을 기반으로 지정된 패키지 내에서 어노테이션이 부착된 클래스를 탐색 (명시하지 않으면 @ComponentScan을 선언한 클래스의 패키지가 기준)
Comment on lines +38 to +65

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌🏻 이해가 잘되어요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 리뷰 감사합니다!


## **[단위 테스트와 통합 테스트 탐구]**

### 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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2가지 DTO를 중첩 클래스로 구현하신 것 같은데 이유와 내부 코드가 궁금해요~!!

혹시 컨트롤러 파라미터로 사용하는 DTO와
컨트롤러->서비스로 데이터를 넘길때 사용하는 DTO를 하나의 파일로 묶고 싶은 의도였을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 개인적으로 DTO를 만들 때 프로젝트 스타일에 따라서 달라지는 것 같은데요! 위는 제가 사용한 예시인데, 여기에서는 class 단위로 requestDTO를 관리하고 그 안에서 컨트롤러 마다 세부 DTO를 따로 정의해서 한 코드 안에 정리를 했습니다 XD

accountSaveReqDto.setNumber(1111L);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO를 불변 객체로 관리해야 한다 VS 아니다
중 후자이신가요?? 저도 후자쪽이지만 그 이유가 궁금합니다

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 DTO를 프로젝트 마다 다르게 설정하는데, 이 부분에서는 test 코드여서 Dummy 데이터를 넣었다! 라는 의미로 저렇게 setter을 사용했어요!

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를 사용
- 스프링 컨텍스트에 존재하는 기존 빈을 대체하여 등록
-
14 changes: 14 additions & 0 deletions src/test/java/com/ceos21/spring_boot/ApplicationTests.java
Original file line number Diff line number Diff line change
@@ -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() {

}

}
Original file line number Diff line number Diff line change
@@ -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());
}
}