Back-End/React.js, 스프링 부트, AWS로 배우는 웹 개발 101

[React.js, 스프링 부트, AWS로 배우는 웹 개발 101][백엔드 개발] - 컨트롤러 레이어, 서비스 레이어, 퍼시스턴스 레이어

얄루몬 2022. 6. 9. 02:28

본 포스팅은 'React.js, 스프링 부트, AWS로 배우는 웹 개발 101 - 김다정'님의 책을 보고 작성되었습니다.


목차
1. 컨트롤러 레이어: 스프링 REST API 컨트롤러 
2. 서비스 레이어: 비지니스 로직
3. 퍼시스턴스 레이어: 스프링 데이터 JPA
4. 정리

컨트롤러 레이어: 스프링 REST API 컨트롤러 

  • HTTP는 GET/POST/PUT/DELETE/OPTIONS 등과 같은 메서드와 URI를 이용해 서버에 HTTP 요청을 보낼 수 있다.
  • 해당 요청을 받은 서버는 어떤 HTTP 메서드를 이용했는지를 알아야 하고 그 후 해당 리소스의 HTTP 메서드에 연결된 메서드를 실행해야 한다.
    • 스프링부트의 spring web(라이브러리)의 어노테이션을 이용하면 이 연결을 쉽게할 수 있다.

@RestController

  • REST API를 구현하기에 @RestController를 사용해 이 컨트롤러가 RestController임을 명시해준다.
  • @RestController를 사용하면 http와 관련된 코드 및 요청과 응답 매핑을 스프링이 알아서 해준다.
package com.example.demo.controller;


import com.example.demo.dto.ResponseDTO;
import com.example.demo.dto.TestRequestBodyDTO;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

//JSON 형태로 객체를 돌려주기 위한 용도로 쓰이는 애너테이션이다.
@RestController

//http://localhost:8080/test로 경로를 매핑 시켜준다
@RequestMapping("test") //리소스
public class TestController {
    @GetMapping
    public String testController(){
        return "Hello world!";
    }
}

@GetMapping("/경로 지정")

@GetMapping("/testGetMapping")
public String testControllerWithPath(){
    return "Hello world! testGetMapping";
}
  • localhost:8080/test/testGetMapping으로 GET요청이 있다면 testControllerWithPath()가 실행된다.

매개변수를 넘겨 받는 방법

    @GetMapping("/{id}")
    //@PathVariable(required = false) 매개변수가 꼭 필요한 것이 아니라는 의미로 id를 명시하지 않아도 에러가 나지 않는다.
    public String testControllerWithPathVariables(@PathVariable(required = false) int id){
        return "Hello world! ID " + id;
    }

    // @PathVariable과 동일하지만 ?id={id}와 같이 요청 매개변수로 넘어오는 값을 변수로 받을 수 있다.
    //http://localhost:8080/test/testRequestParam?id=123
    @GetMapping("/testRequestParam")
    public String testControllerWithRequestParam(@RequestParam(required = false) int id){
        return "Hello world! ID " + id;
    }

    //http://localhost:8080/test/testRequestBody
    //Body를 JSON으로 바꿔서 넣어주어야 제대로 확인이 된다.
    @GetMapping("/testRequestBody")
    //JSON 형태로 HTTPRESPONSE에 담아 반환해주는 애너테이션이다.
    public String testControllerRequestBody(@RequestBody TestRequestBodyDTO testRequestBodyDTO) {
        return "Hello world! ID " + testRequestBodyDTO.getId() + " Message : " + testRequestBodyDTO.getMessage();
    }
  • @PathVariable을 사용해 URI 경로로 넘어오는 값을 변수로 받을 수 있다.
  • @RequestParam 역시 요청 매개변수로 넘어오는 값을 변수로 받을 수 있다.
  • @RequestBody를 사용한 방법은 반환하고자 하는 리소스가 복잡할 때 사용한다.
    • 오브젝트처럼 복잡한 자료형을 요청에 보내고 싶을 때 사용한다.

응답으로 오브젝트를 리턴하는 방법

    //ResponseDTO를(= 즉, 객체를 반환한다는 뜻이다.) 반환하는 컨트롤러
    //http://localhost:8080/test/testResponseBody
    @GetMapping("/testResponseBody")
    public ResponseDTO<String> testControllerResponseBody(){
        List<String> list = new ArrayList<>();
        list.add("Hello world! I'm ResponseDTO");

        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
        return response;
    }

    //엔티티를 반환하는 컨트롤러 메서드(이것도 뭐 객체를 반환하는 것임)
    @GetMapping("/testResponseEntity")
    public ResponseEntity<?> testControllerResponseEntity(){
        List<String> list = new ArrayList<>();
        list.add("Hello world! I'm ResponseEntity. And you got 404");

        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();

        //http 상태 코드를 404로 설정해준다.
        return ResponseEntity.badRequest().body(response);
    }
  • @RestController
    • @Controller + @ResponseBody
    • @Controller
      • 스프링이 이 클래스의 오브젝트를 알아서 생성하고 다른 오브젝트들과 의존성을 연결한다는 의미이다.
    • @ResponseBody
      • 클래스의 메서드가 리턴하는 것은 웹 서비스의 ResponseBody라는 의미이다. 
      • 다시 말해, 메서드가 리턴할 때 스프링은 리턴된 오브젝트를 JSON 형태로 바꾸고 HttpResponse에 담아 반환한다는 의미이다.

 

서비스 레이어: 비지니스 로직

  • 서비스 레이어는 컨트롤러와 퍼시스턴스 사이에서 비지니스 로직을 수행하는 역할을 한다.
  • 서비스 레이어는 HTTP와 긴밀히 연관된 컨트롤러에서 분리되어 있고 또 데이터베이스와 긴밀히 연관된 퍼시스턴스와 분리돼 있다.
  • 서비스 레이어는 우리가 개발하고자 하는 로직에 집중할 수 있게 해준다.

Todo서비스 구현

package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class TodoService {
    public String testService(){
        return "Test Service";
    }
}
  • @Service 애노테이션 안에는 @Component 애노테이션을 가지고 있고 기능 차이는 없다.
    • 스프링 컴포넌트이며 기능적으로 비지니스 로직을 수행하는 서비스 레이어임을 알려주는 애노테이션이다.

TodoController

package com.example.demo.controller;


import com.example.demo.dto.ResponseDTO;
import com.example.demo.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("todo")
public class TodoController {

    //빈을 알아서 찾고 그 빈을 이 인스턴스 멤버 변수로 연결하라는 뜻이다.
    @Autowired
    private TodoService service;

    @GetMapping("/test")
    public ResponseEntity<?> testTodo(){
        String str = service.testService();//테스트 서비스 사용
        List<String> list = new ArrayList<>();
        list.add(str);

        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
        return ResponseEntity.ok().body(response);

    }


}

서비스 레이어 테스팅

 

퍼시스턴스 레이어: 스프링 데이터 JPA

  • 아이템을 데이터베이스에 저장해야한다. 이때 중간 작업이 너무 많아 이를 해결하기 위해서 JPA가 등장했고 JPA는 보다 더 쉽게 자바 코드로 된 엔티티 클래스와 테이블을 매핑해주는 역할 등을 한다.
  • ORM은 데이터베이스를 자바 코드(객체지향 언어)등으로 조작할 수 있게 하는 것을 의미한다

엔티티 클래스

  • 엔티티 클래스는 클래스 그 자체가 테이블을 정의해야 한다.
  • ORM이 엔티티를 보고 어떤 테이블의 어떤 필드에 매핑해야 하는지 알 수 있어야 한다는 뜻이다.
  • 기본키, 외래키 구분도 할 수 있어야 한다.

자바 클래스를 엔티티로 정의할 때 주의해야 할 점

  • 클래스에는 매개변수가 없는 생성자 NoArgsConstructor가 필요하다.
  • Getter/Setter가 필요하다.
  • 기본키를 지정해주어야 한다.

엔티티 클래스

package com.example.demo.model;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;


//빌더 패턴 생성을 해주는 애너테이션
@Builder

//매개변수가 없는 생성자를 구현해준다.
@NoArgsConstructor

//클래스의 모든 멤버 변수를 매개변수로 받는 생성자를 구현
@AllArgsConstructor

// 멤버 변수의 setter/ getter 메서드 구현
@Data

//현재 클래스가 엔티티 클래스임을 명시해준다.
@Entity

//테이블 이름 지정
@Table(name = "Todo")
public class TodoEntity {
    //기본키 지정
    @Id
    // ID를 자동생성하겠다는 의미이다. generator로 어떤 방식으로 ID를 자동생성할지 지정
    @GeneratedValue(generator = "system-uuid")
    //하이버네트가 제공하는 하는 것이 아닌 나만의 generator를 사용하고 싶을 때 사용한다.
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id; //이 오브젝트의 아이디
    private String userId; // 이 오브젝트를 생성한 사용자의 아이디
    private String title;
    private boolean done; // todo를 완료한 경우를 위한 멤버 변수


}
  • @Builder
    • 빌더 패턴 생성을 해주는 애너테이션
  • @NoArgsConstructor
    • 매개변수가 없는 생성자를 구현해준다.
  • @AllArgsConstructor
    • 클래스의 모든 멤버 변수를 매개변수로 받는 생성자를 구현
  • @Data
    • 멤버 변수의 setter/ getter 메서드 구현
  • @Entity
    • 현재 클래스가 엔티티 클래스임을 명시해준다.
  • @Table(name = "Todo")
    • 테이블 이름 지정
  • @Id
    • 기본키 지정
  • @GeneratedValue(generator = "system-uuid")
    • ID를 자동생성하겠다는 의미이다. generator로 어떤 방식으로 ID를 자동생성할지 지정
  • @GenericGenerator(name = "system-uuid", strategy = "uuid")
    • 하이버네트가 제공하는 하는 것이 아닌 나만의 generator를 사용하고 싶을 때 사용한다.

이제 JPA가 Entity 클래스와 데이터베이스의 테이블을 매칭하여 매핑해준다.

TodoRepository

package com.example.demo.persistence;

import com.example.demo.model.TodoEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
    
}
  • JpaRepository 인터페이스를 사용하기 위해서는 새 인터페이스를 작성해 JpaRepository 를 확장해야 한다.
  • 이때 JpaRepository는 제네릭 타임을 받는 것을 주의하자!
  • @Repository 애노테이션도 @Component 애노테이션의 특별 케이스로 해당 클래스를 루트 컨테이너에 빈(Bean) 객체로 생성해주는 애노테이션이다.

JpaRepository 인터페이스

/*
 * Copyright 2008-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.jpa.repository;

import java.util.List;

import javax.persistence.EntityManager;

import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;

/**
 * JPA specific extension of {@link org.springframework.data.repository.Repository}.
 *
 * @author Oliver Gierke
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Sander Krabbenborg
 * @author Jesse Wouters
 * @author Greg Turnquist
 */
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll()
	 */
	@Override
	List<T> findAll();

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
	 */
	@Override
	List<T> findAll(Sort sort);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
	 */
	@Override
	List<T> findAllById(Iterable<ID> ids);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
	 */
	@Override
	<S extends T> List<S> saveAll(Iterable<S> entities);

	/**
	 * Flushes all pending changes to the database.
	 */
	void flush();

	/**
	 * Saves an entity and flushes changes instantly.
	 *
	 * @param entity entity to be saved. Must not be {@literal null}.
	 * @return the saved entity
	 */
	<S extends T> S saveAndFlush(S entity);

	/**
	 * Saves all entities and flushes changes instantly.
	 *
	 * @param entities entities to be saved. Must not be {@literal null}.
	 * @return the saved entities
	 * @since 2.5
	 */
	<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

	/**
	 * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
	 * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
	 * method.
	 *
	 * @param entities entities to be deleted. Must not be {@literal null}.
	 * @deprecated Use {@link #deleteAllInBatch(Iterable)} instead.
	 */
	@Deprecated
	default void deleteInBatch(Iterable<T> entities) {
		deleteAllInBatch(entities);
	}

	/**
	 * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
	 * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
	 * method.
	 *
	 * @param entities entities to be deleted. Must not be {@literal null}.
	 * @since 2.5
	 */
	void deleteAllInBatch(Iterable<T> entities);

	/**
	 * Deletes the entities identified by the given ids using a single query. This kind of operation leaves JPAs first
	 * level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this method.
	 *
	 * @param ids the ids of the entities to be deleted. Must not be {@literal null}.
	 * @since 2.5
	 */
	void deleteAllByIdInBatch(Iterable<ID> ids);

	/**
	 * Deletes all entities in a batch call.
	 */
	void deleteAllInBatch();

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
	 */
	@Deprecated
	T getOne(ID id);

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
	 * @since 2.5
	 */
	@Deprecated
	T getById(ID id);

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @since 2.7
	 */
	T getReferenceById(ID id);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
	 */
	@Override
	<S extends T> List<S> findAll(Example<S> example);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
	 */
	@Override
	<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
  • JpaRepository의 첫 번째 매개변수 T는 테이블에 매핑될 엔티티 클래스를 넣어주어야 한다.
  • JpaRepository의 두번째 매개변수 ID는 이 엔티티의 기본 키 타입을 넣어주어야 한다. (우리 엔티티의 경우 String이어서 String이 들어간다.)

@Service와 @Repository의 차이?

  • 컨트롤러 : @Controller (프레젠테이션 레이어, 웹 요청과 응답을 처리함)
  • 로직 처리 : @Service (서비스 레이어, 내부에서 자바 로직을 처리함)
  • 외부I/O 처리 : @Repository (퍼시스턴스 레이어, DB나 파일같은 외부 I/O 작업을 처리함)

출처: https://codevang.tistory.com/258

 

@Repository, @Service 어노테이션

- Develop OS : Windows10 Ent, 64bit - WEB/WAS Server : Tomcat v9.0 - DBMS : MySQL 5.7.29 for Linux (Docker) - Language : JAVA 1.8 (JDK 1.8) - Framwork : Spring 3.1.1 Release - Build Tool : Maven 3.6..

codevang.tistory.com

TodoService에서 TodoRepository 사용

package com.example.demo.service;

import com.example.demo.model.TodoEntity;
import com.example.demo.persistence.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TodoService {

    @Autowired
    private TodoRepository repository;


    public String testService(){

        //TodoEntity 생성
        TodoEntity entity = new TodoEntity().builder().title("My first todo item").build();

        //TodoEntity 저장
        repository.save(entity);

        //TodoEntity 검색
        TodoEntity savedEntity = repository.findById(entity.getId()).get();


        return savedEntity.getTitle();
    }
}

레포지토리 테스트 결과

[어떻게 위와 같은 결과가 나온 것일까?]

  • JpaRepository는 기본적인 데이터베이스 오퍼레이션 인터페이스를 제공한다.
    • 이는 JPA 관련 서적이나 관련 강의 등을 보고 좀 알아두면 좋다!!!
  • save, findById, findAll 등이 기본적으로 제공되는 인터페이스에 해당한다.
  • 구현은 스프링 데이터 JPA가 실행 시에 알아서 해준다.
  • 또한 JpaRepository는 구현 클래스 없이도 사용할 수 있는데 이 이유는 스프링이 methodInterceptor이라는 AOP 인터페이스를 사용해 가능한 일이라고 한다.
  • 또한 스프링은 우리가 JpaRepository를 사용하려 부르면 이 call을 가로채 본인이 쿼리를 작성해준다고 한다. (우리가 직접 쿼리문을 작성할 일이 없다.)

정리

  • 퍼시스턴스 레이어
    • 데이터베이스와 통신하며 필요한 쿼리를 보내고 해석해 엔티티 오브젝트로 변환해 주는 역할
  • 서비스 레이어
    • HTTP나 데이터베이스 가은 외부 컴포넌트로부터 추상화돼 우리가 온전히 비지니스 로직에만 집중할 수 있게 해주는 역할
  • 컨트롤러 레이어
    • 주로 HTTP 요청과 응답을 어떻게 넘겨 받고 리턴하는냐(응답), 즉 외부 세계와 통신하는 규약을 정의하는 역할