1️⃣ ORM
- ORM 이란 Object-Relational Mapping 의 약자로
- 객체(Object)와 관계형 데이터(Relational data) 를 매핑하기 위한 기술
- 관계형 데이터베이스와 객체 지향 프로그래밍은 서로 패러다임이 달라 패러다임 불일치가 발생함
- 객체 지향
- 필드와 메서드 등을 묶어서 객체로 잘 만들어 사용하는 것이 목표
- 객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다.
- 관계형 데이터베이스
- 데이터를 잘 정규화해서 보관하는 것이 목표
- 객체 지향
- 이를 해결하기 위해 ORM 기술이 필요하고 JPA는 Java Persistence API의 약자로, 자바 ORM 기술에 대한 API 표준 명세이다.
2️⃣ JPA
- 자바 ORM 기술에 대한 API 표준 명세, 인터페이스 모음
- JPA는 애플리케이션과 JDBC 사이에서 동작한다.
- 개발자가 JPA를 사용하면, JPA 내부에서 JDBC API를 사용하여 SQL을 호출하여 DB와 통신한다.
- 즉, 개발자가 직접 JDBC API를 쓸 필요가 없다.
💚 Hibernate
- JPA를 구현한 프레임워크 중 사실상 표준이다.
- 오픈소스 소프트웨어이다.
- JPA는 기술 스펙이고 하이버네이트는 이 기능을 구현하여 공급해주는 역할이다.
💚 Spring Data JPA
- Spring framework에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트(모듈)이다.
사용하는 이유로는 - 구현체 교체의 용이성: Hibernate 외에 다른 구현체로 쉽게 교체하기 위함
- 저장소 교체의 용이성: 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함
3️⃣ 프로젝트에 Spring Data Jpa 적용하기
*주의*
필자는 이미 3장 코드를 끝까지 작성한 뒤 이 글을 작성하였음.
단계별로 써놓을 여력이 없어(...) 한번에 작성했으니 참고만 할 것
단계별 필요한 부분이 아닌 전부 복붙해서 따라하시면 오류 폭탄을 맞을 수 있습니다...
💚 build.gradle에 코드 추가해주기
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mustache'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.testng:testng:7.1.0'
testImplementation 'junit:junit:4.13.1'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation('org.projectlombok:lombok')
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2'
}
📌 spring-boot-starter-data-jpa
- 스프링 부트용 Spring Data Jpa 추상화 라이브러리입니다.
- 스프링 부트 버전에 맞춰 자동으로 JPA관련 라이브러리들의 버전을 관리해 줍니다.
📌 h2
- 인메모리 관계형 데이터베이스입니다.
- 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있습니다
- 메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용됩니다.
- 이 책에서는 JPA의 테스트, 로컬 환경에서의 구동에서 사용할 예정입니다.
💚 Posts 클래스 작성
package com.jojoldu.book.springboot.domain.posts;
import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String author;
@Builder
public Posts(String title, String content, String author){
this.title=title;
this.content=content;
this.author=author;
}
public void update(String title, String content){
this.title=title;
this.content=content;
}
}
📌 @Entity
- 테이블과 링크될 클래스임을 나타냅니다.
- 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭합니다.
ex) SalesManager.java > sales_manager table
📌 @Id
- 해당 테이블의 PK 필드를 나타냅니다.
📌 @GeneratedValue
- PK의 생성 규칙을 나타냅니다.
- 스프링 부트 2.0 에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 됩니다.
📌 @Column
- 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 됩니다.
- 사용하는 이유는, 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용합니다.
- 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶거나(ex: title), 타입을 TEXT로 변경하고 싶거나(ex: content) 등의 경우에 사용됩니다.
Entity의 PK는 Long 타입의 Auto_increment를 추천
주민등록번호, 복합키 등은 유니크 키로 별도로 추가하시는 것을 추천
📌 @NoArgsConstructor
- 기본 생성자 자동 추가.
- public Posts(){}와 같은효과
📌 @Getter
- 클래스 내 모든 필드의 Getter 메소드를 자동생성
📌 @Builder
- 해당클래스의 빌더 패턴클래스를 생성
- 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함
이 Posts 클래스에 Setter 메소드가 없는 이유?
Java Bean 규약을 생각하면서 getter/setter를 무작정 생성하는 경우가 있음.
이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어, 차후 기능 변경 시 정말 복잡해짐
//잘못 된 예시
public class Order{
public void setStatus(boolean status){
this.status = status
}
}
public void 주문서비스의_취소이벤트 (){
order.setStatus(false); //외부 함수의 기능이 class의 setter로 값을 변경하고 있다.
}
//올바른 예시
public class Order{
public void cancelOrder(){
this.status = false;
}
}
public void 주문서비스의_취소이벤트 (){ //외부함수에서 class 멤버 함수를 사용하여 이벤트를 처리하고 있다.
order.cancelOrder();
}
Setter가 없는데 DB에 어떻게 값을 넣어 insert할수 있을까?
기본 구조는 생성자를 통해 최종 값을 채운 뒤 DB 삽입하는 것, 만약 값 변경이 필요시 해당 이벤트에 맞는 public 메소드를 호출하고 변경
이 책에서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용함
생성자와 빌더의 차이점 간단 설명
생성 시점에 값을 채워주는 역할은 같음
빌더의 경우 생성자와 다르게 값을 넣을 때 패턴에 맞게 명확하게 넣어줄 수 있다.
예를 들어
생성자 : new Example(b, a)
이렇게 a와 b의 위치를 변경해도 코드 실행 전까지 문제를 찾을 수 없음
public Example(String a, String b){
this.a=a;
this.b=b;
}
빌더는 어느 필드에 어떤 값을 채워야할지 명확하게 인지 가능
Example.builder()
.a(a)
.b(b)
.build();
Builder에 관한 자세한 설명은 여기서 확인!
4️⃣ Spirng Data JPA 테스트 코드 작성하기
💚 PostsRepositoryTest 코드 작성
package com.jojoldu.book.springboot.domain.posts;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After
public void cleanup(){
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기(){
//given
String title="테스트 게시글";
String content="테스트 본문";
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("kny@swu.ac.kr")
.build());
//when
List<Posts> postsList=postsRepository.findAll();
//then
Posts posts=postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
@Test
public void BaseTimeEntity_등록(){
//given
LocalDateTime now = LocalDateTime.of(2023,9,24,0,0,0);
postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
//when
List<Posts> postsList=postsRepository.findAll();
//then
Posts posts=postsList.get(0);
System.out.println(">>>>>>>> createDate="+posts.getCreatedDate()+", modifiedDate="+posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
}
📌 @After
- Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정
- 보통은 배포전 전체테스트를 수행할때 테스트간 데이터침범을 막기위해 사용합
니다. - 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아있어 다음테스트실행시 테스트가 실패할 수 있습니다.
📌 postsRepository.save
- 테이블 posts에 insert/update 쿼리를 실행합니다.
- id 값이 있다면 update가, 없다면 insert 쿼리가 실행됩니다.
📌 postsRepository.findAll
- 테이블 posts에 있는 모든 데이터를 조회해오는 메소드입니다.
💚 application.properties 수정
spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
5️⃣ 등록/수정/조회 API 만들기
- API를 만들기 위해 총 3개의 클래스가 필요
- 1. Request 데이터를 받을 Dto
- 2. API 요청을 받을 Controller
- 3. 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
📌 Web Layer
- 흔히 사용하는 컨트롤러(@Controller)와 JSP/Freemarker 등의 뷰 템플릿 영역이다.
- 이외에도 필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등
외부 요청과 응답에 대한 전반적인 영역을 이야기한다.
📌 Service Layer
- @Service에 사용되는 서비스 영역이다.
- 일반적으로 Controller와 Dao의 중간 영역에서 사용된다.
- @Transactional이 사용되어야 하는 영역이기도 하다.
📌 Repository Layer
- Database와 같이 데이터 저장소에 접근하는 영역이다.
- Dao(Data Access Obejct) 영역으로 이해하면 된다.
📌 Dtos
- Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체를 말하며,
Dtos는 이들의 영역을 뜻한다. - 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 해당된다.
📌 Domain Model
- 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고 한다.
- 택시 앱을 예시로 들었을 때, 배차, 탑승, 요금 등이 모두 도메인이 될 수 있다.
- @Entity가 사용된 영역 역시 도메인 모델이라고 이해하면 된다.
- 다만, VO처럼 값 객체들도 이 영역에 해당되므로 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아니다.
+ 추가
- 많은 사람들이 오해하고 있는 것이, Service에서 비즈니스 로직을 처리해야 한다는 것이다.
- 하지만, 전혀 그렇지 않다. Service는 트랜잭션, 도메인 간 순서 보장의 역할만 한다.
- Web(Controller), Service, Repository, Dto, Domain, 이 5가지 레이어에서 비즈니스 처리를 담당해야 할 곳은 어디일까?
바로 Domain이다. - 기존에 서비스로 처리하던 방식을 트랜잭션 스크립트라고 한다.
@Transactional
public Order cancelOrder(int orderId){
//1) 데이터베이스로부터 주문정보(Orders), 결제정보(Billing), 배송정보(Delivery) 조회
OrdersDto order = ordersDao.selectOrders(orderId);
BillingDto billing = billingDao.selectBilling(orderId);
DeliveryDto delivery = deliveryDao.selectDelivery(orderId);
//2) 배송 취소를 해야 하는지 확인
String deliveryStatus = delivery.getStatus();
//3) if(배송중이라면) -> 배송취소로 변경
if("IN_PROGRESS".equals(deliveryStatus)){
delivery.setStatus("CANCEL");
deliveryDao.update(delivery);
}
//4) 각 테이블에 취소 상태 Update
order.setStatus("CANCEL");
ordersDao.update(order);
billing.setStatus("CANCEL");
deliveryDao.update(billing);
return order;
}
- 모든 로직이 서비스 클래스 내부에서 처리된다
- 서비스 계층이 무의미 객체란 단순히 데이터 덩어리 역할만 하게 된다.
반면 도메인 모델에서 처리할 경우
@Transactional
public Order cancleOrder(int orderId){
//1)
Orders order = ordersRepository.findById(orderId);
Billing billing = billingRepository.findById(orderId);
Delivery delivery = deliveryRepository.findByOrderId(orderId);
//2-3)
delivery.cancle();
//4)
order.cancle();
billing.cancle();
return order;
}
- order, billing, delivery가 각자 본인의 취소 이벤트 처리를 함
- 서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장해 줌
💚 트랜잭션? @Transactional
- 쪼갤 수 없는 여러 작업들을 논리적으로 묶은 최소 단위로 묶은 것
- 모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용 + 어떤 작업에서 오류가 발생했을 때는 이전에 있던 모든 작업들을 완전히 되돌리는 것이 트랜잭션의 개념임
- 데이터베이스를 다룰 때 트랜잭션을 적용하면 데이터 추가, 갱신, 삭제 등으로 이루어진 작업을 처리하던 중 오류가 발생했을 때 모든 작업들을 원상태로 되돌릴 수 있음
@Service
public class PostService {
@Transactional
public Long addPost(Long boardId, NewPostRequest newPostRequest, AuthInfo authInfo) {
// 새로운 게시글을 등록하는 비즈니스 로직만을 작성
}
@Transactional
public void updatePost(Long postId, PostUpdateRequest postUpdateRequest, AuthInfo authInfo) {
// 게시글을 수정하는 비즈니스 로직만을 작성
}
6️⃣ 등록 API 생성
PostApiController 작성
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.service.posts.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
return postsService.update(id, requestDto);
}
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById (@PathVariable Long id){
return postsService.findById(id);
}
}
- 클라이언트에서 "/api/v1/posts"로 요청이 오면 service를 호출해서 Long 값을 리턴 받는다.
- RestController로 Json데이터를 리턴한다.
PostService 작성
package com.jojoldu.book.springboot.service.posts;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
@Transactional
public Long update(Long id, PostsUpdateRequestDto responseDto){
Posts post=postsRepository.findById(id).
orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
post.update(responseDto.getTitle(), responseDto.getContent());
return id;
}
public PostsResponseDto findById (Long id){
Posts entity=postsRepository.findById(id)
.orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
return new PostsResponseDto(entity);
}
}
앞선 코드(PostService) 에서 생성자는 어디있을까?
바로 스프링 Bean 등록 시 생성자 주입 방식을 위해 @RequiredArgsConstructor 어노테이션을 사용!
(final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복의 이 어노테이션이 대신 생성해준다.)
롬복 어노테이션을 사용하는 이유는 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위해서이다.
롬복 어노테이션이 있으면 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거하는 등의 상황이 발생해도 생성자 코드는 전혀 손대지 않아도 된다.
스프링에서 Bean 주입 방식
@Autowired → 권장하지 않는다.
setter
생성자 → 가장 권장하는 방식!
스프링에서 Bean을 주입받는 세가지 방법(@Autowired, Setter, 생성자)
@RequiredArgsConstructor 어노테이션을 사용한 생성자 주입
에 대한 자세한 설명은 여기서!
PostSaveRequestDto 작성
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title=title;
this.content=content;
this.author=author;
}
public Posts toEntity(){
return Posts.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
Entity 클래스를 Request/Response 클래스로 사용하면 안 된다!
- 여기서 Entity 클래스와 거의 유사한 형태임에도 Dto 클래스를 추가로 생성했다.
Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스이기 때문에 이를 기준으로 테이블이 생성되고 스키마가 변경된다. - 화면 변경은 아주 사소한 기능 변경인데 이를 위해 테이블과 연관된 Entity 클래스를 변경하는 것은 너무 큰 변경이다.
- Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만, Request와 Response용 Dto는 View를 위한 클래스라 정말 자주 변경이 필요하다.
그러므로 Entity 클래스와 Controller에서 쓸 Dto는 분리해서 사용해야 한다.
Entity 클래스에 대한 자세한 설명은 여기서
PostApiControllerTest 작성
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@After
public void tearDown() throws Exception{
postsRepository.deleteAll();
}
@Test
public void Posts_등록된다() throws Exception{
//given
String title="title";
String content="content";
PostsSaveRequestDto requestDto=PostsSaveRequestDto.builder()
.title(title)
.content(content)
.author("author")
.build();
String url="http://localhost:" + port + "/api/v1/posts";
//when
ResponseEntity<Long> responseEntity=restTemplate.postForEntity(url, requestDto, Long.class);
//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all=postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
@Test
public void Posts_수정된다() throws Exception{
//given
Posts savedPosts=postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updateId=savedPosts.getId();
String expectedTitle="title2";
String expectedContent="content2";
PostsUpdateRequestDto requestDto= PostsUpdateRequestDto.builder()
.title(expectedTitle)
.content(expectedContent)
.build();
String url="http://localhost:" + port + "/api/v1/posts/"+updateId;
HttpEntity<PostsUpdateRequestDto> requestEntity=new HttpEntity<>(requestDto);
//when
ResponseEntity<Long> responseEntity=restTemplate.
exchange(url, HttpMethod.PUT, requestEntity, Long.class);
//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all=postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
ApiController를 테스트하는데 HelloController와 달리 @WebMvcTest를 사용하지 않는다.
- @WebMvcTest의 경우 JPA 기능이 작동하지 않기 때문인데,
Controller와 ControllerAdvice 등 외부 연동과 관련된 부분만 활성화된다. - 이렇게 JPA 기능까지 한 번에 테스트 할 때는 @SpringBootTest와 TestRestTemplate를 사용하면 된다.
WebEnvironment.RANDOM_PORT로 인한 랜덤 포트 실행과 insert 쿼리가 실행된 것까지 모두 확인했다!
💻 Reference
💻 Next Post
'Server > Spring Boot' 카테고리의 다른 글
[Spring Boot] 빌더(Builder) 패턴의 정의, 생성자 패턴 빌더 패턴 비교 (0) | 2023.09.25 |
---|---|
[Spring] 스프링 의존성 주입 종류와 @RequiredArgsConstructor 어노테이션을 사용한 생성자 주입 (0) | 2023.09.24 |
[Chapter 02] 스프링 부트에서 테스트 코드를 작성하자 (0) | 2023.09.16 |
[Chapter 01] 인텔리제이로 스프링 부트 시작하기 (0) | 2023.09.15 |
[Spring Boot] 스프링의 콘셉트(IoC, DI, AOP, PSA) (0) | 2023.09.13 |