이번 주차에서는 스프링에서 CRUD와 관련해서 패키지 구조와 역할에 대해 학습하고, DI, IoC, Bean에 대해서도 학습 했다.
CRUD란 컴퓨터가 기본적인 데이터 처리를 하는 기능인 Create(생성), Read(읽기 or 조회), Update(수정), Delete(삭제)를 뜻한다. 클라이언트와 서버간 HTTP 프로토콜을 이용해 RESTful하게 데이터를 전송할 때도 CRUD개념이 활용된다.
이름 | 조작 | Method |
---|---|---|
Create | 생성 | POST |
Read | 조회 | GET |
Update | 수정 | PUT |
Delete | 삭제 | DELETE |
스프링에서 CRUD를 구성하기 위해 패키지 구조를 생성할 때 contorller, entity, dto, service, repository 5가지로 나뉜다.
클라이언트에서 요청하면 응답해주는 곳이다. 클라이언트가 전달한 데이터가 있다면 전달 받은 데이터를 가공하여 업무를 수행하고 완료되면 View에 전달한다.
DB의 테이블에 존재하는 Column들을 필드로 가지는 객체를 뜻한다. Entity는 DB의 테이블과 1대1 대응이며, 테이블에 가지지 않는 Column을 필드로 가져서는 안 된다. 또한 Entity 클래스는 다른 클래스를 상속받거나 인터페이스의 구현체여서는 안되고 순수한 데이터 객체인 것이 좋다.
DTO는 말 그대로 데이터를 Transfer(이동)하기 위한 객체이다. Client가 Controller에 요청을 보낼 때도 RequestDto의 형식으로 데이터가 이동하고, Controller가 Client에게 응답을 보낼 때도 ResponseDto의 형태로 데이터를 보내게 된다.
Controller와 Service, Repository 계층 사이에 데이터가 오갈 때도 데이터는 DTO의 형태로 이동하게 된다.
DTO는 로직을 갖고 있지 않는 순수한 데이터 객체이며, 일반적으로 getter/setter 메서드만을 가진다. 하지만 DTO는 단순히 데이터를 옮기는 용도이기 때문에 굳이 Setter를 이용해 값을 수정할 필요가 없이, 생성자만을 사용하여 값을 할당하는 게 좋다.
Service 계층은 DTO를 통해 받은 데이터를 이용해 비즈니스 로직(기능 담당)을 처리하고, DAO(혹은 Repository)를 통해 DB에 접근하여 데이터를 관리하는 역할을 한다.
JPA를 사용하면 Repository를 통해 DB에 실제로 접근할 수 있다. Service와 DB를 연결해주는 역할을 하며, Service 계층에서 Repository를 이용하여 데이터를 관리할 수 있다.
객체를 생성 후 직접적으로 접근하는 것이 아닌 외부로 통해 생성된 객체를 접근하여 주입 시켜주는 것이다. 이를 통해 객체 간의 결합도가 낮아지고 유연성이 높아져 프로그램을 유지 및 보수하는데 있어 용이해진다.
즉 IoC 컨테이너라는 매개체를 두고 여기에 필요한 모든 모듈들을 등록해둔다. 사용처가 직접 생성하는 것이 아니라 필요할 때 IoC 컨테이너가 모듈들을 주입 해주는 방식. 이 과정에서 의존 하는 모듈에서 생성과 해제 주입 등의 일련의 제어 과정을 IoC 컨테이너 기능을 포함하는 프레임워크들에게 줌으로써 제어의역전이 일어난다
장점
의존성 감소
코드양 감소
테스트 용이
의존성 주입 방법 3가지
위 3가지 방법은 느슨한결합 예시에 포함되어 있다. 예제코드로 DI 장점도 같이 확인해볼 수 있다
강하게 결합된 객체는 다른 객체에 대한 많은 정보를 필요로 하며 두 객체 간의 인터페이스들에게 서로 높은 의존성을 가지고 있다
예제
public class Person {
private Chicken chicken;
public Person() {
this.chicken = new Chicken();
}
public void startEat() {
chicken.eat();
}
}
class Chicken {
public void eat() {
System.out.println("치킨을 먹습니다.");
}
}
위 코드는 Person 클래스가 로직을 수행하기 위해 Chicken 클래스를 사용하고 있다. 그러면 Person 클래스를 사용하기 위해 Chicken 클래스가 반드시 필요하며 이 관계를 class 간의 의존 관계라고 할 수 있으며 'Person이 Chicken을 의존한다' 라고 표현할 수 있다.
Person 클래스 안에 Chicken 클래스를 Hambuger 클래스로 변경을 한다고 했을 때 코드 변경을 확인해보자
public class Person {
private Hambuger hambuger;
public Person() {
this.hambuher = new Hambuger();
}
public void startEat() {
hambuger.eat();
}
}
class Hambuger {
public void eat() {
System.out.println("햄버거를 먹습니다.");
}
}
멤버 변수 타입(클래스) 하나만 변경 했을 뿐인데 Person 클래스에 있는 대다수의 코드가 변경 되었다.
그렇기 때문에 강한 결합이라고 하고 강한 결합은 유지보수 측면에서 좋지 않은 것을 볼 수 있다.
클래스의 자료구조, 메서드를 추상화할 수 있는 인터페이스 클래스를 이용하여 클래스 간의 의존성을 최소화 한다
<Food.java>
public interface Food {
void eat();
}
<Chicken.java>
public class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹습니다.");
}
}
<Hambuger.java>
public class Hambuger implements Food {
@Override
public void eat() {
System.out.println("햄버거를 먹습니다.");
}
}
<Person.java>
public class Person {
private Food food; // 필드변수 이용
public Person(Food food) { // 생성자 이용
this.food = food; // setter 이용
}
public void startEat() {
food.eat();
}
}
Food 인터페이스를 만들어 eat() 추상 메서드를 선언하여 Chicken 클래스와 Hambuger 클래스를 구현체로 구현하면 Chicken 객체와 Hambuger 객체는 모두 Food 타입에 대입이 가능하여 유연하게 사용할 수 있다.
이렇게 되면 Person 클래스 내부적으로는 코드 변경이 일어날 필요가 없어지고 생성자를 통해 객체를 받아 멤버변수에 대입하기만 하면 객체 변경이 가능하게 된다
Spring IoC 컨테이너가 관리하는 자바 객체를 빈이라고 부른다. 이전 글에서 제어의 역전(IOC, Inversion Of Control)에 대해 알아봤는데 간단하게 설명 했었다. 자바 프로그래밍에서는 Class를 생성하고 new 키워드를 이용하여 객체를 생성한 후 사용했습니다. 하지만 Spring에서는 직접 new 키워드를 이용하여 객체를 생성하는게 아닌 Spring에 의하여 관리당하는 자바 객체를 사용한다.(이것이 스프링이 객체의 생성 제어권을 가져가는 것으로 IoC 제어의 역전을 뜻함) 이렇게 Spring에 의해 생성되고 관리되는 자바 객체를 Bean이라고 한다.
흔히 스프링 부트에서 사용하는 @RestController, @Service 등의 어노테이션을 통해 특정 객체를 빈으로 등록해 사용하는 것이다.
컨트롤러, 서비스, 리포지토리를 사용할 수 있게 예제코드로 의존관계를 설정해보았다.
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
생성자에 @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에 찾아서 넣어준다. 이처럼 객체 의존관계를 외부에서 넣어주는 것을 '의존성 주입(DI)'라 한다.
위 그림에서 보면 membercontroller --> memberService 의존관계를 표현한 화살표 역할을 해준다.
이 상태로 서버를 작동시키면 오류가 발생한다. 이유는 memberService도 bean을 등록시켜줘야 하기 때문이다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
memberRepository도 스프링 빈으로 등록한다.
@Repository
public class MemoryMemberRepository implements MemberRepository {
}
스프링은 IoC 컨테이너에 스프링 bean을 등록할 때 기본적으로 싱글톤으로 등록한다.(유일하게 하나만 등록해서 공유)
즉 같은 스프링 bean이면 모두 같은 인스턴스이다. 설정으로 싱글톤이 아니게 설정할 수 있지만 특별한 경우를 제외하면 대부분 싱글톤을 사용
지금 현 기준 2.7.5v에서는 의존성 주입을 lombok의 @RequiredArgsConstructor 어노테이션을 이용하여 더 편리하게 의존성 주입을 하고 있다.
@RequiredArgsConstructor
final 또는 @Notnull이 붙은 필드의 생성자를 자동으로 만들어준다. 이를 통해 새로운 필드를 추가할 때 다시 생성자를 만들거나 하는 등의 번거로움이 없어졌다. 하지만 자동적으로 생성자가 만들어지기 때문에 내가 예상하지 못한 결과나 오류가 발생할 수 있기 때문에 그런 점도 염두해둬야 한다.
위에 코드와 다르게 아래 코드를 보면 더욱 더 간결해진다.
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
}
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
}
참조
https://steady-hello.tistory.com/120
https://velog.io/@damiano1027/Java-%EA%B0%95%ED%95%9C-%EA%B2%B0%ED%95%A9%EA%B3%BC-%EC%95%BD%ED%95%9C-%EA%B2%B0%ED%95%A9
https://nudg.tistory.com/13