Product.java
)Product.java
package kr.co.hanbit.product.management.domain;
public class Product {
private Long id;
private String name;
private Integer price;
private Integer amount;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Integer getPrice() {
return price;
}
public Integer getAmount() {
return amount;
}
}
클래스 이름은 대문자로 시작, 필드명은 소문자로 시작
Product의 모든 필드가 private이다. 때문에 필드에 접근할 수가 없음!! 컨트롤러가 응답을 주기 위해서는 응답으로 제공하려는 필드에 대한 접근 메서드를 만들어주기 위해 getter
메서드를 만들어준다.
Lombok 어노테이션 사용하면... 될 것 같은데 왜 안쓰지? 라고 생각했지만 그냥 책에서 하라는 대로 따라갔다. 뭔가 다 가르침이 있겠지!
만일 접근을 위해 필드의 접근 제어자를 public으로 만들어버릴 경우 캡슐화가 깨진다. setter, getter를 만드는 것도 캡슐화가 깨지기는 마찬가지이지만... 일단 넘어가.
long
vs. Long
long은 원시 타입(Primitive Type), Long은 클래스 (Wrapper Class)
원시 타입은 언어 차원에서 지원하는 기본 타입으로, 특정 클래스가 아닌 순수한 타입이다.
래퍼 클래스는 말 그대로 클래스로, long과 같은 범위의 자료형이지만 기본 타입이 아니라 클래스로 존재한다.
그렇다면, 식별자(상품번호)에 래퍼 클래스를 사용하는 이유는 무엇일까?
null
값을 가질 수 있다.프리미티브 타입은 null이 될 수 없다. 때문에 받은 요청 JSON에 특정 필드가 '없다'라는 상태를 나타낼 수가 없기 때문에, 컬렉션에 값을 넣고 싶은 상황이 생겼을 때 문제가 된다.
→ 가격(price), 재고 수량(amount)이 정의되지 않은 상태를 null
로 표현하기 위해서는 Integer
와 같은 래퍼 클래스를, 정의되지 않은 상태를 허용하지 않고 0으로 초기화하려면 int
를 사용하면 된다.
또한 프리미티브 타입과 래퍼 클래스의 변환 과정은 박싱(Boxing)과 언박싱(Unboxing)이라고 한다. 프리미티브 타입을 래퍼 클래스로 넣는 경우를 박싱, 그 반대가 언박싱이다. 이 과정이 불필요하게 반복될 경우 애플리케이션의 성능을 떨어뜨리기도 한다.
ProductController.java
package kr.co.hanbit.product.management.presentation;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class ProductController {
@RequestMapping(value = "/products", method = RequestMethod.POST)
public Product createProduct(@RequestBody Product product) {
return product;
}
}
이를 통해서 간단히 컨트롤러에서 응답으로 Product를 받아볼 수 있다.
JSON을 인스턴스로, 인스턴스를 JSON으로 변환하는 것은 누가 해주나요?
👉🏻 스프링 프레임워크 자체적으로 'HTTP 메시지 컨버터(Http Message Converters)'가 있다! 이 HTTP 메시지 컨버터 중 JSON을 변환해주는 것이 'MappingJackson2HttpMessageConverter'이다.이는 스프링에서 자동으로 등록해 준 메시지 컨버터로, 사용자가 별도의 HTTP 메시지 컨버터를 정의하고 등록하여 사용할 수도 있다.
하나의 소프트웨어를 여러 개의 계층으로 나누고 각 계층의 책임과 역할을 구분하여 여러 가지 제약을 두는 설계 방법, 소프트웨어를 유지보수하기 좋게 만드려는 노력의 일환!
표현(Presentation) 계층
Interface 계층이라고도 부른다. 클라이언트로부터 들어오는 요청을 받고 응답해주는 역할로, 컨트롤러가 전형적인 표현 계층에 속하는 존재이다.
데이터가 표현계층으로 들어오면, 이 데이터가 값이 존재하는지, 타입이 맞는지 등 간단한 유효성 검사를 마친 후 응용 계층으로 처리를 넘긴다.
응용(Application) 계층
표현 계층에서 넘겨받은 데이터로 새로운 자원을 저장하거나 저장되어 있던 자원을 조회해 온다. 여기서 자원이란, 도메인 객체(Domain Object)를 의미한다. 조회된 도메인 객체의 메서드를 실행시키며, 주로 'Service'라고 불리는 코드가 위치한다.
도메인(Domain) 계층
도메인의 핵심적인 지식이 있는 계층으로, 도메인 객체가 위치한다. (ex. Product 클래스) 도메인의 핵심적인 지식이란 비즈니스와 관련된 요구사항을 말한다.
도메인 계층은 다른 계층이나 외부 요소에 의존하지 않도록 만들어야 한다.
인프라스트럭처(Infrastructure) 계층
특정 인프라스트럭처에 접근하는 구현 코드들이 위치하는 계층이다. 리스트에 상품을 저장하는 로직이나 데이터베이스에 상품을 저장하는 코드 등이 포함된다.
애플리케이션에서 사용되는 데이터와 그 데이터를 다루는 로직을 하나로 묶은 것
단순한 데이터와 로직이 아니라 애플리케이션의 핵심 지식을 포함하고 있다. (비즈니스 로직이나 도메인 규칙, ex. '상품은 재고 수량 이상으로 주문할 수 없다'와 같은 개념)
만일 비즈니스 로직이 도메인 객체에 모이지 않고, 서비스 코드에 구현된다면 도메인 객체의 응집력이 낮아지고 서비스 코드와의 결합도가 증가해 점점 중복된 코드가 늘어난다. 당연히 유지보수가 어려운 코드가 된다.
스프링 프레임워크에서 사용되는 개념으로, 스프링 컨테이너가 관리하는 자바 객체를 말한다. 스프링이 생성하고 제어하는 객체를 빈이라고 말한다.
스프링 컨테이너가 빈의 생성, 초기화, 설정, 소멸 등의 생명 주기를 관리한다. 주로 서비스, 데이터 접근 객체(DAO), 컨트롤러 등 애플리케이션의 주요 구성 요소로 사용된다.
@Configuration
, @Bean
어노테이션)을 이용한 빈 등록import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@Component
, @Service
, @Repository
, @Controller
어노테이션)을 이용한 빈 등록 import org.springframework.stereotype.Component;
@Component
public class MyBean {
// 빈으로 등록될 클래스
}
클래스에 어노테이션을 붙여 빈을 등록한 후
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
// 컴포넌트 스캔 설정
}
스프링 설정 클래스에서 컴포넌트 스캔을 활성화한다. 여기서 @Component
어노테이션이 붙은 클래스는 자동으로 빈으로 등록된다. @Service
, @Repository
, @Controller
어노테이션도 동일한 방식으로 사용할 수 있다.
객체지향 프로그래밍에서 객체 간의 의존성을 외부에서 주입해주는 디자인 패턴을 말한다.
객체가 직접 다른 객체를 생성하거나 관리하지 않고, 외부에서 필요한 객체를 주입받아 사용하는 방식이다.
이 개념이... 자바를 처음 공부할 때 정말 어려웠던 것 같다. 객체가 뭔지, 의존성이 뭔지 도통 알 수가 없었으니 ^^...
의존성
: 클래스가 다른 클래스의 기능을 사용해야 할 때, 그 클래스가 필요로 하는 객체를 의존성이라고 한다. 주입
: 필요한 객체를 직접 생성하지 않고, 외부에서 제공받는 것을 말한다. public class MyService {
// final 키워드를 사용하면 해당 필드가 한 번 초기화 된 후 변경될 수 없다. -> 불변성!
private final MyRepository myRepository;
@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
public class MyService {
// final 키워드를 사용하지 않아서 의존성을 가변으로 유지한다.
private MyRepository myRepository;
// 디폴트 생성자 (명시적으로 정의하지 않으면 컴파일러가 자동으로 추가)
public MyService() {
}
// 세터 메서드를 통한 의존성 주입
@Autowired
public void setMyRepository(MyRepository myRepository) {
this.myRepository = myRepository;
}
public void performService() {
myRepository.doSomething();
}
}
public class MyService {
@Autowired
private MyRepository myRepository;
}
@Service
빈 등록SimpleProductServie.java
package kr.co.hanbit.product.management.application;
import org.springframework.stereotype.Service;
@Service
public class SimpleProductService {
}
단순히 @Service
라는 코드를 추가한 것만으로도 해당 클래스는 스프링 컨테이너에 의해 생성되어 관리된다. 이제 이 의존성을 필요하는 곳에서 주입받아 사용하면 된다.
ProductController.java
package kr.co.hanbit.product.management.presentation;
import kr.co.hanbit.product.management.application.SimpleProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
// 생략
@RestController
public class ProductController {
private SimpleProductService simpleProductService;
@Autowired
ProductController(SimpleProductService simpleProductService) {
this.simpleProductService = simpleProductService;
}
@RequestMapping(value = "/products", method = RequestMethod.POST)
public Product createProduct(@RequestBody Product product) {
return product;
}
}
의존성을 주입할 때는 이렇게 일반적으로 '생성자 주입' 방식을 많이 사용한다.
@Repository
빈 등록ListProductRepository.java
package kr.co.hanbit.product.management.infrastructure;
import kr.co.hanbit.product.management.domain.Product;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Repository
public class ListProductRepository {
private List<Product> products = new CopyOnWriteArrayList<>();
public Product add(Product product) {
products.add(product);
return product;
}
}
@Repository
역시 빈으로 등록하기 위한 애너테이션이다. 각 의존성이 역할을 나눠주고 추가적인 활용이 가능하다.
CopyOnWriteArrayList
를 사용한 것은 웹 애플리케이션이 동시에 동작하는 멀티 스레드라는 특수한 환경 때문에 스레드 세이프한 컬렉션을 사용해야 하기 때문이다. ArrayList는 스레드 세이프하지 않은 컬렉션이다.
이후 Service코드에서 Repository를 의존성 주입 받아 호출하도록 코드를 고쳐본다.
package kr.co.hanbit.product.management.application;
(생략)
@Service
public class SimpleProductService {
private ListProductRepository listProductRepository;
@Autowired
SimpleProductService(ListProductRepository listProductRepository) {
this.listProductRepository = listProductRepository;
}
public ProductDto add(Product product) {
Product savedProduct = listProductRepository.add(product);
return savedProduct;
}
}
우선은 단순하게 ListProductRepository를 주입받아서 호출하는 정도의 기능을 하는 상태다.
ProductController.java
package kr.co.hanbit.product.management.presentation;
(생략)
@RestController
public class ProductController {
private SimpleProductService simpleProductService;
@Autowired
ProductController(SimpleProductService simpleProductService) {
this.simpleProductService = simpleProductService;
}
@RequestMapping(value = "/products", method = RequestMethod.POST)
public ProductDto createProduct(@RequestBody Product product) {
return simpleProductService.add(product);
}
}
id
추가하기Product의 id는 어디에서 관리해야 할까?
전자의 경우 데이터베이스를 사용하도록 애플리케이션을 수정할 때마다 Product를 변경해야 한다. 때문에 ListProductRepository에서 관리하도록 하는 것이 좋다. (도메인 계층은 다른 계층이나 외부 요소에 의존하지 말아야 한다는 원칙)
Product.java
package kr.co.hanbit.product.management.domain;
public class Product {
private Long id;
private String name;
private Integer price;
private Integer amount;
public void setId(Long id) {
this.id = id;
}
(생략)
}
먼저 Product에 id 필드에 대한 setter
를 추가한다. 도메인 객체에 대한 setter와 getter는 캡슐화가 깨진 코드를 만들 수 있어 가급적 피해야 하지만, id에 대한 관리 책임을 Product가 아닌 ListProductRepository로 가져오면서 외부에서 id를 설정해야 할 필요가 있기 때문에 setter를 추가한다.
ListProductRepository.java
package kr.co.hanbit.product.management.infrastructure;
(생략)
@Repository
public class ListProductRepository {
private List<Product> products = new CopyOnWriteArrayList<>();
private AtomicLong sequence = new AtomicLong(1L);
public Product add(Product product) {
product.setId(sequence.getAndAdd(1L));
products.add(product);
return product;
}
}
이어서 ListProductRepository 코드도 다음과 같이 수정한다. 상품 번호를 1부터 1씩 증가한다는 요구사항에 맞게 1로 초기화하고, Product의 id는 getAndAdd() 메서드로 값을 가져온 후 1씩 증가시키는 연산을 사용한다.
AtomicLong 역시 스레드 안정성을 가지는 클래스로 Long을 안전하게 사용할 수 있게 만들어준다.
이를 통해 응답으로 오는 상품의 id가 1씩 증가하도록 만들 수 있다.
빈 등록, 의존성 주입에 대한 이야기는 처음 자바를 배울 때 꽤나 생소하고 이해가 가지 않는 내용이었다. 빈...? 콩...? 뭘 주입해...? 혼돈 그 자체...
아마도 그 때는 객체라는 것 자체가 전혀 감도 잡히지 않은 상태로 뭘 배우려다 보니 더더욱 이해가 가지 않았던 것 같다.
지금도 완전히 이해한 것은 아니지만 객체는 클래스를 통해 만들어지는 것... 클래스의 인스턴스라고 할 수 있을 것 같다. 클래스라는 설계도에 맞춰서 인스턴스가 생성된다.
빈이라는 것도 결국엔 객체다. 다만 스프링 컨테이너가 만들고 관리하는 것이다. 계층 간 공유되어야 하는 자원을 (외부에서 필요한 객체를) 의존성 주입을 통해 쉽게 제공받을 수 있다.
후 알듯말듯... 공부해가는 중. 그래도 한 번 다시 보니까 조금은 반갑네...
레이어드 계층에 대해서도 다시 자세히 설명을 뜯어볼 수 있어서 좋았다. 왜냐면... 예전엔 진짜 알못이라 뭐가 뭔지도 모르고 만들었음. 용어가 조금 낯선 감이 없잖아 있지만! 공부합시다~
<이것이 백엔드 개발이다 with 자바> 책을 공부하고 정리한 내용입니다.