오봉이와 함께하는 개발 블로그

스프링 week_3 본문

자바/스프링 튕튕

스프링 week_3

오봉봉이 2021. 11. 2. 17:40
728x90

artineer_week_lecture_3

JPA

  • JPA는 ORM형태의 기술이다. 기존에는 DB에 대한 의존도가 높기 때문에 DB Query에 의한 시스템을 구축하였다

  • 자바는 객체지향 언어이기 때문에 DB와 다른 용도로 사용해야 한다.

    • 그래서 ORM이라는 기술이 발전하게 되었다.
  • ORM(Objective Relation Mapping) : Java의 객체와 DB의 Relation을 Mapping한다 하여 ORM이다. 벤더에 독립적으로 만들 수 있어 DB 프로그램과 분리되어 의존도가 떨어진다.

  • JPA implementation : implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

  • H2 Database : 메모라에 저장하는 DB로 테스트를 하거나 예제를 진행하는 프로젝트에서 간단하게 사용할 수 있다.

    • H2 Database implementation : implementation 'com.h2database:h2'
  • @Entity - JPA가 관리하는 Domain이라고 알리기 위한 어노테이션으로 DB에 새로 생긴 객체를 생성이나 제거해준다.

  • @Id - PK로 사용하기 위한 어노테이션

  • @GeneratedValue - PK를 어떤 전략으로 사용한다 명시하는 어노테이션

  • @NoArgsConstructor - 생성자의 파라미터가 아무 것도 없는 기본 생성자를 만드는 어노테이션

  • @AllArgsConstructor - Builder를 위해 명시한 어노테이션으로 없으면 Builder에 오류가 생긴다.

JPA를 위해 엔티티로 변경

Article.java

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String title;
    String content;

    public void update(String title, String content) {
        this.title = title;
        this.content = content; // 도메인에 등록하여 JPA의 관리하에 둔다.
    }
  • 도메인에 CRUD하기 위해 관리하는 객체를 Repository라고 한다. Repository는 인터페이스로 생성.

ArticleRepository.java

public interface ArticleRepository extends CrudRepository<Article, Long> {
}
//CrudRepository 스프링 JPA에서 제공하는 CRUD를 바로 사용할 수 있도록 해줌. 
// <T(리포지토리가 관리하는 도메인, Id(리포지토리가 관리하는 도메인의 id)>
  • ArticleService 코드를 ArticleRepository 객체와 연계될 수 있는 코드(JPA)

ArticleService.java

@RequiredArgsConstructor
@Service
public class ArticleService {
  private final ArticleRepository articleRepository;

  public Long save(Article request) {
    return articleRepository.save(request).getId();
  }

  public Article findById(Long id) {
    return articleRepository.findById(id).orElse(null);
  }

  @Transactional
  public Article update(Article request) {
    Article article = this.findById(request.getId());
    article.update(request.getTitle(), request.getContent());

    return article;
  }

  public void delete(Long id) {
    Article article = this.findById(id);
    articleRepository.delete(article);
  }
}
  • JPA는 하나의 Transaction이 끝날 때 마다 갱신하게 된다.

  • @Transactional : 하나의 Transaction을 알리는 어노테이션

CRUD API

ArticleController.java

@RequiredArgsConstructor
@RequestMapping("/api/v1/article")
@RestController
public class ArticleController {
    private final ArticleService articleService;

// …
@PutMapping("/{id}")
  public Response<ArticleDto.Res> put(@PathVariable Long id, @RequestBody ArticleDto.ReqPut request) {
    Article article = Article.builder()
            .id(id)
            .title(request.getTitle())
            .content(request.getContent())
            .build();

    Article articleResponse = articleService.update(article);

    ArticleDto.Res response = ArticleDto.Res.builder()
            .id(String.valueOf(articleResponse.getId()))
            .title(articleResponse.getTitle())
            .content(articleResponse.getContent())
            .build();

    return Response.<ArticleDto.Res>builder().code(ApiCode.SUCCESS).data(response).build();
  }

  @DeleteMapping("/{id}")
  public Response<Void> delete(@PathVariable Long id) {
    articleService.delete(id);
    return Response.<Void>builder().code(ApiCode.SUCCESS).build();
  }
}

H2 Database

  • 메모라에 저장하는 DB로 테스트를 하거나 예제를 진행하는 프로젝트에서 간단하게 사용할 수 있다.

application.properties

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb

Of pattern

  • 정적 팩토리메소드 패턴이라고도 한다.

  • Controller에서 했던 일을 Article에서 하게 해준다. - Article 객체 생성 코드가 Controller에 있기 때문에 개선이 필요.

  • 비즈니스 로직에 대하여 응집도를 높여준다.

  • 가독성을 높여줄 수 있고 객체지향스러운 코드 작성 가능.

Article.java

class Article {
    // ...
  public static Article of(ArticleDto.ReqPost from) {
    return Article.builder()
            .title(from.getTitle())
            .content(from.getContent())
            .build();
  }

  public static Article of(ArticleDto.ReqPut from, Long id) {
    return Article.builder()
            .id(id)
            .title(from.getTitle())
            .content(from.getContent())
            .build();
  }
}

ArticleDto.java

//…
public static Res of(Article from) {
            return Res.builder()
                    .id(String.valueOf(from.getId()))
                    .title(from.getTitle())
                    .content(from.getContent())
                    .build();
        }
    }
} // ArticleController.java에 GET과 PUT에 있던 코드 중복 제거 ( of pattern )

Response.java

@Getter
@Builder
public class Response<T> {
    private ApiCode code;
    private T data;

    public static Response<Void> ok() {
        return Response.<Void>builder()
                .code(ApiCode.SUCCESS)
                .build();
    }
    public static <T> Response<T> ok(T data) {
        return Response.<T>builder()
                .code(ApiCode.SUCCESS)
                .data(data)
                .build();
    }
} // ArticleController.java에 GET과 POST에 있던 코드 중복 제거 ( of pattern )

ArticleController.java

class ArticleController {
    //...
  @PostMapping
  public Response<Long> post(@RequestBody ArticleDto.ReqPost request) {
    return Response.ok(articleService.save(Article.of(request)));
  }

  @GetMapping("/{id}")
  public Response<ArticleDto.Res> get(@PathVariable Long id) {
    return Response.ok(ArticleDto.Res.of(articleService.findById(id)));
  }

  @PutMapping("/{id}")
  public Response<ArticleDto.Res> put(@PathVariable Long id, @RequestBody ArticleDto.ReqPut request) {
    return Response.ok(ArticleDto.Res.of(articleService.update(Article.of(request, id))));
  }

  @DeleteMapping("/{id}")
  public Response<Void> delete(@PathVariable Long id) {
    articleService.delete(id);
    return Response.ok();
  }
}
// 기존 길고 중복이 많던 코드를 가독성이 좋고 중복을 제거한 코드로 변경

예외처리

  • 실무에서는 오류가 발생하면 오류를 나타내면 안된다.

  • 개발자는 정상 케이스와 비정상 케이스 모두 잡아내어야 한다.

  • NullPointerException이 발생하는 것은 너무 위험하기 때문에 적절한 예외처리가 필요하다.

ArticleService.java

class ArticleService {
    // ...
  @Transactional
  public Article update(Article request) {
    Article article = this.findById(request.getId());

    if(Objects.isNull(article)) {
      throw new RuntimeException("article value is not existed.");
    }

    article.update(request.getTitle(), request.getContent());

    return article;
  }
} // 실제 API에 대한 내용이 담기는 예외처리가 아님.
실제 API에 대한 내용이 담기는 예외처리가 아님.

ApiCode.java

@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ApiCode {
    /* COMMON */
    SUCCESS("CM0000", "정상입니다"),
    DATA_IS_NOT_FOUND("CM0001", "데이터가 존재하지 않습니다")
    ;
//…
}

ArticleController.java

class ArticleController {
  //...
  @PutMapping("/{id}")
  public Response<Object> put(@PathVariable Long id, @RequestBody ArticleDto.ReqPut request) {
    try {
      return Response.ok(ArticleDto.Res.of(articleService.update(Article.of(request, id))));
    } catch (RuntimeException e) {
      return Response.builder().code(ApiCode.DATA_IS_NOT_FOUND).data(e.getMessage()).build();
    }
  }
} // DATA_IS_NOT_FOUND라는 오류만 출력함.
  • DATA_IS_NOT_FOUND라는 오류만 출력함.
    • 유연성이 부족하다.

ApiException.java

@Getter
public class ApiException extends RuntimeException {
    private final ApiCode code;

    public ApiException(ApiCode code) {
        this.code = code;
    }

    public ApiException(ApiCode code, String msg) {
        super(msg);
        this.code = code;
    }
}

ArticleService.java

class ArticleService {
    //...
  @Transactional
  public Article update(Article request) {
    Article article = this.findById(request.getId());

    if (Objects.isNull(article)) {
      throw new ApiException(ApiCode.DATA_IS_NOT_FOUND, "article value is not existed.");
    }

    article.update(request.getTitle(), request.getContent());

    return article;
  }
  //...
}

ArticleController.java

class ArticleController {
  @PutMapping("/{id}")
  public Response<Object> put(@PathVariable Long id, @RequestBody ArticleDto.ReqPut request) {
    try {
      return Response.ok(ArticleDto.Res.of(articleService.update(Article.of(request, id))));
    } catch (ApiException e) {
      return Response.builder().code(e.getCode()).data(e.getMessage()).build();
    }
  }
}
  • API코드에 따라 예외처리를 받을 수 있지만, try-catch문의 중복코드가 발생하여 가독성이 떨어진다.

  • 여러가지 문제들 때문에 Spring에서는 ControllerAdvice를 제공한다.

  • ControllerAdvice는 클라이언트의 요청에 따라 컨트롤러가 처리하고 응답이 내려가게 될 때 컨트롤러 다음 단계에서 예외처리를 묶어서 같이 해줄 수 있는 기능을 제공한다.

ControllerExceptionHandler.java

@RestControllerAdvice
public class ControllerExceptionHandler {
    @ExceptionHandler(ApiException.class)
    public Response<String> apiException(ApiException e) {
        return Response.<String>builder().code(e.getCode()).data(e.getMessage()).build();
    }
}
  • @ExceptionHandler : 예외처리에 대한 핸들링을 해달라는 어노테이션.

ArticleController.java

class ArticleController {
    //...
  @GetMapping("/{id}")
  public Response<ArticleDto.Res> get(@PathVariable Long id) {
    return Response.ok(ArticleDto.Res.of(articleService.findById(id)));
  }

  @PutMapping("/{id}")
  public Response<ArticleDto.Res> put(@PathVariable Long id, @RequestBody ArticleDto.ReqPut request) {
    return Response.ok(ArticleDto.Res.of(articleService.update(Article.of(request, id))));
  }
}
  • Assert 객체를 통해 예외처리를 하면 if문을 줄여 가독성을 높일 수 있다.

Assert.java

public class Asserts {
    public static void isNull(@Nullable Object obj, ApiCode code, String msg) {
        if(Objects.isNull(obj)) {
            throw new ApiException(code, msg);
        }
    }
}

ArticleService.java

public class ArticleService {
    //...
  @Transactional
  public Article update(Article request) {
    Article article = this.findById(request.getId());
    Asserts.isNull(article, ApiCode.DATA_IS_NOT_FOUND, "article value is not existed.");

    article.update(request.getTitle(), request.getContent());

    return article;
  }
}
  • 자바 자체에서 제공하는 Optional객체는 정해준 경우에 따라 무조건적인 처리를 진행한다.

  • 하지만 비용이 많이 발생하므로 중요한 비즈니스 로직에 사용을 권장한다.

  • 조금 더 단순한 형태에서는 Assert사용.

  • ArticleService.java

    class ArticleService {
      // ...
    public Article findById(Long id) {
      return articleRepository.findById(id)
              .orElseThrow(() -> new ApiException(ApiCode.DATA_IS_NOT_FOUND, "article value is not existed."));
    }
    
    @Transactional
    public Article update(Article request) {
      Article article = this.findById(request.getId());
      article.update(request.getTitle(), request.getContent());
    
      return article;
    }
    }
728x90

'자바 > 스프링 튕튕' 카테고리의 다른 글

스프링 week_6  (0) 2021.11.22
스프링 week_5  (0) 2021.11.15
스프링 week_4  (0) 2021.11.02
스프링 week_2  (0) 2021.11.02
스프링 week_1  (0) 2021.11.02
Comments