[TIL] 13. Java Exception과 Spring 기반 CRUD 아키텍처

MinseoKim·6일 전
post-thumbnail

1. 오늘의 한 줄 요약

이번 강의는 예외 처리에 대한 개념 학습 이후 Spring Boot를 이용한 CRUD 애플리케이션의 구조를 설계하는 과정을 배운다.

데이터베이스와 연동하기 전 단계에서 MVC 패턴과 Spring Framework의 핵심 구성 요소, 그리고 적용되는 디자인 패턴을 배웠다.


2. 배운 내용

1) 예외(Exception)

  • Error: 논리적 오류, etc…
  • Exception: mild error (예기지 못한 상황)

예외 발생 시점

  1. Complie(IOE Exception)
  2. Runtime(RuntimeException)

예외 클래스 상속 계층도

  • 가장 상위 클래스 : Exception

출처: https://devlog-wjdrbs96.tistory.com/141


예외 처리 방법

try-catch

주의할 점

  • 순차적 검사를 실행하므로 상위 catch 블록에서 예외 처리가 발생하면 하위의 모든 catch 블럭은 실행되지 않음
  • 따라서 여러개의 catch 블록을 작성해야 하는 경우 예외 클래스의 하위 클래스를 먼저 배치하여 구체적인 예외 처리를 먼저 검토하도록 작성해야한다.
try {
    // 예외가 발생할 가능성이 있는 코드
} catch (ExceptionType1 e1) {
    // ExceptionType1 예외가 발생했을 경우 실행할 코드
} catch (ExceptionType2 e2) {
    // ExceptionType2 예외가 발생했을 경우 실행할 코드
} finally {
    // 예외 발생 여부와 상관없이 항상 실행
}
public class ExceptionApp {

    public static void main(String[] args) {
        System.out.println("debug >>>> main start");

        String[] strAry = {"jslim","inspire","lgcns"};

        try {
            for(int idx = 0; idx <= strAry.length; idx++) {
                System.out.println(strAry[idx]);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("debug >>>> 예외 발생 시에만 수행");
            e.printStackTrace();
        } finally {
            System.out.println("debug >>>> 예외 발생 여부와 상관 없이 수행");
        }

        System.out.println("debug >>>> main end");
    }
}

throws

예외를 호출한 쪽으로 다시 예외를 떠넘기는 것

public void xxxxxx() throws xxxxException {
    예외코드
}

// 사용자 정의 예외 클래스
public class xxxxException extends Exception {
}
throw

의도적으로 예외 발생

package features.exception;

import features.util.InspireException;

public class exceptionDemo {

    public void first(int x) throws InspireException {
        System.out.println("debug >>>> first method start");

        try {
            if(x < 0) {
                throw new InspireException("양의 정수만 가능합니다");
            }
        } finally {
            System.out.println("debug >>>> first method end");
        }
    }
}

throw와 throws

---> 보완할 부분


2) 스프링 기반 CRUD 아키텍처

Spring MVC

  • 요청 → DispatcherServlet(프론트 컨트롤러) → URL 매핑 → 실제 컨트롤러 → 서비스 → 저장소 → DB
  • Controller → Service → Repository(DAO)의 3층 구조
  • Spring Boot는 Spring MVC를 가장 핵심으로 구현한다.
  • MVC 패턴을 통해 프론트, 비즈니스 로직, 데이터 접근을 명확히 분리함
FrontController (DispatcherServlet)
  • 모든 HTTP 요청은 DispatcherServlet에 도착
  • URL → 컨트롤러 매핑을 Key-Value형으로 관리
  • 실제 컨트롤러는 Factory를 통해 생성되고 프론트가 호출

ORM 및 저장소

  • MyBatis / JPA(Hibernate): 두 가지 대표 ORM 프레임워크 (구현체)

    • 실제 DB 연동
    • Repository는 인터페이스만 선언하고
    • 구현체는 프레임워크가 제공
  • Repository에서 Entity를 조회/저장하고

  • DTO를 통해 컨트롤러와 서비스 간에 데이터 교환


패턴

  • Session Facade: 프론트 컨트롤러와 실제 컨트롤러 사이의 매핑 및 라우팅을 담당
  • Factory Method: 컨트롤러 인스턴스 생성·관리
  • Singleton: 한 번만 인스턴스화된 객체를 재사용
  • Static Method Pattern: DTO ↔ Entity 변환(정적 메서드)
  • DTO Pattern: 데이터 전송을 위한 객체
  • MVC Pattern: 전반적 구조
  • Command Pattern: 인터페이스를 통한 표준화된 연산 정의

패키지 구조 예시

├─ controller
│  └─ BlogController
├─ service
│  └─ BlogService
├─ repository
│  └─ BlogRepository
├─ domain
│  ├─ dto
│  └─ entity
├─ util
│  └─ ScannerUtil
└─ view
   └─ ConsoleView

cf. scanner → next / nextLine : 공백 포함


3) 기억해야 할 기본 용어들

용어
DTO (Data Transfer Object)데이터를 담아 계층 간에 전달하는 용도로만 쓰임
비즈니스 로직 없음
필드 + getter/setter만 있음
DAO (Data Access Object)데이터베이스에 직접 접근해서 CRUD 쿼리를 실행하는 객체
실제 데이터를 찾아오는 것
ORM (Object-Relational Mapping)DB의 테이블(행/열)을 자바의 객체(Object)로 자동 매핑해주는 기술/도구
DB 테이블 (row)
↕ // ORM이 자동 변환
Entity 객체 (Dao/Repository 내부에서 다룸)
↓ // 변환해서 담기
DTO (Controller ↔ Service ↔ View 사이를 이동)

3. 실습

1) 예외 처리 과제

미션

  • 문자열로 부터 문자열 정수가 아닌 문자열이 입력된다면 예외 발생
  • 예외 처리를 통해서 메시지를 출력하고 다시 입력 받을 수 있도록 흐름을 제어

내 답변

public void landingPage() {

    while(true) {
        System.out.println();
        System.out.println(">>>> Inspire Camp Blog Ver(1.0) <<<<");
        System.out.println("1. 전체 검색");
        System.out.println("2. 게시글 상세보기");
        System.out.println("3. 입력화면");
        System.out.println("4. 수정화면");
        System.out.println("5. 게시글 삭제");
        System.out.println("6. 검색");
        System.out.println("99. 프로그램 종료");

        try {
            System.out.print("원하시는 메뉴 번호를 입력하세요 : ");
            int number = Integer.parseInt(scan.nextLine());

            // case가 많을때
            switch(number) {
                case 99:
                    exit();
                    break;

                default:
                    break;
            }

        } catch(Exception e) {
            System.out.println(">>> 예외 발생으로 인해 프로그램을 종료합니다.");
            break;
        }
    }
}

2) Spring MVC 구조 직접 구현

실습 전체 개요

  • 목적: Spring MVC 구조를 스프링 없이 손수 구현해보는 것
main()
  → BlogReactView.landingPage()  (사용자 입력 받는 화면)
    → BlogFrontController.list()  (요청을 받아서 분배)
      → BlogBeanFactory.getBean()  (컨트롤러 객체를 찾아줌)
        → ListController.list()  (컨트롤러 PL)
          → BlogReactService.list()  (비즈니스 로직 BL)
            → BlogReactDao.findByAll()  (데이터 저장소 접근)

백엔드 구조

PL (Presentation Layer)
  • controller

    • front controller에게 요청을 받아 BL에 일을 시킨 뒤 결과를 다시 프론트로 전달
BL (Business Layer)
  • service

    • 현재 dao 호출
PL (Persistent Layer)
  • Repository/dao

    • 데이터 접근
    • ORM 쿼리

유의

  • 백엔드의 각 계층은 서로간만 정보전달이 가능하다.

    • PL ↔ PL (x)
    • ListControllerBlogReactDao를 몰라야 함

3) DTO

  • 데이터를 옮기기 위한 그릇 역할만 하는 객체
  • 로직 없고 필드 + getter/setter만 있음

BlogRequestDTO

package features.blogs.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class BlogRequestDTO {

    private int id;
    private String title;
    private String content;
    private String email;
}

BlogResponseDTO

package features.blogs.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class BlogResponseDTO {

    private int status;
    private String message;

    ////////////////////////////////////////

    private Integer blogId;
    private String title, content, email;
    private Integer viewCnt;
}

4) App

import features.blogs.view.BlogReactView;

public class BlogReactApp {

    public static void main(String[] args) {
        BlogReactView view = new BlogReactView();
        view.landingPage();
    }
}

5) Front Controller — BlogFrontController

  • 모든 요청이 이 클래스를 거쳐 들어옴
  • endPoint를 통해 알맞은 컨트롤러를 찾아 실행시키는 역할
  • (ListController)controller : 다운캐스팅
  • BlogBeanFactory.getInstance() : 객체를 딱 하나만 만들고 계속 재사용
package features.blogs.facade;

import java.util.List;

import javax.swing.ListCellRenderer;

import features.blogs.controller.ListController;
import features.blogs.domain.dto.BlogResponseDTO;
import features.blogs.factory.BlogBeanFactory;

public class BlogFrontController {

    private BlogBeanFactory factory;

    public BlogFrontController() {
        factory = BlogBeanFactory.getInstance(); // 싱글톤 패턴
    }

    public List<BlogResponseDTO> list(String endPoint) {
        System.out.println(
            "debug >>>> front controller endPoint : " + endPoint
        );

        Object controller = factory.getBean(endPoint);

        return ((ListController)controller).list();
    }
}

6) Singleton — BlogBeanFactory.getInstance()

Front Controller에서 싱글톤이 필요한 이유

  • BlogBeanFactory"어떤 endPoint에 어떤 컨트롤러가 연결되는지" 정보를 담고 있는 창고
  • 요청마다 다른 창고를 참고하면 요청마다 컨트롤러를 찾을 수도 있고 못 찾을 수도 있는 상황이 생김
  • 따라서 딱 하나의 인스턴스만 만들어 앱 전체가 공유하도록 강제

실제 Spring 구조

클라이언트 요청 (예: GET /blogs/list)
    ↓
DispatcherServlet
← 앱 전체에 "딱 하나"만 존재 (싱글톤처럼 동작)
    ↓
URL을 보고 어떤 컨트롤러 메서드를 실행할지 찾음
    ↓
HandlerMapping
    ↓
@Controller 의 해당 메서드 실행
    ↓
결과를 다시 DispatcherServlet이 받아서 응답으로 내보냄
public class BlogBeanFactory {

    private Map<String, Object> map;

    private static BlogBeanFactory instance;

    private BlogBeanFactory() {
    }

    public static BlogBeanFactory getInstance() {

        if(instance == null) {
            instance = new BlogBeanFactory();
        }

        return instance;
    }
}

7) Controller

  • 필드로 service를 가지고 있다가 자기 메서드가 호출되면 그 일을 Service한테 위임
package features.blogs.controller;

import java.util.List;

import features.blogs.domain.dto.BlogResponseDTO;
import features.blogs.service.BlogReactService;

public class ListController {

    private BlogReactService service;

    public ListController() {
    }

    public ListController(BlogReactService service) {
        this.service = service;
    }

    // view 에게 응답하는 역할의 메서드
    public List<BlogResponseDTO> list() {

        System.out.println(
            "debug >>>> list controller list() "
        );

        return service.list();
    }
}

8) Service

package features.blogs.service;

import java.util.List;

import features.blogs.domain.dto.BlogRequestDTO;
import features.blogs.domain.dto.BlogResponseDTO;
import features.blogs.repository.BlogReactDao;

public class BlogReactServiceImpl implements BlogReactService {

    private BlogReactDao dao;

    public BlogReactServiceImpl() {
    }

    public BlogReactServiceImpl(BlogReactDao dao) {
        this.dao = dao;
    }

    @Override
    public List<BlogResponseDTO> list() {
        System.out.println("debug >>>> blog service list ");
        return dao.findByAll();
    }

    @Override
    public BlogResponseDTO read(int blogId) {
        throw new UnsupportedOperationException(
            "Unimplemented method 'read'"
        );
    }

    @Override
    public int insert(BlogRequestDTO request) {
        throw new UnsupportedOperationException(
            "Unimplemented method 'insert'"
        );
    }

    @Override
    public int update(BlogRequestDTO request) {
        throw new UnsupportedOperationException(
            "Unimplemented method 'update'"
        );
    }

    @Override
    public int delete(int blogId) {
        throw new UnsupportedOperationException(
            "Unimplemented method 'delete'"
        );
    }

    @Override
    public List<BlogResponseDTO> search(BlogRequestDTO request) {
        throw new UnsupportedOperationException(
            "Unimplemented method 'search'"
        );
    }
}

9) Persistent Layer — Repository / DAO

  • DAO(Data Access Object): 데이터베이스에 직접 접근해서 CRUD 쿼리를 실행하는 객체
package features.blogs.repository;

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

import features.blogs.domain.dto.BlogResponseDTO;

public class BlogReactDao {

    private List<BlogResponseDTO> blogs;

    public BlogReactDao() {

        blogs = new ArrayList<>(List.of(

            BlogResponseDTO.builder()
                .blogId(1)
                .title("react")
                .content("state")
                .email("lim")
                .viewCnt(10)
                .build(),

            BlogResponseDTO.builder()
                .blogId(2)
                .title("java")
                .content("oop")
                .email("kim")
                .viewCnt(20)
                .build(),

            BlogResponseDTO.builder()
                .blogId(3)
                .title("spring")
                .content("mybatis")
                .email("lee")
                .viewCnt(30)
                .build(),

            BlogResponseDTO.builder()
                .blogId(4)
                .title("docker")
                .content("devops")
                .email("park")
                .viewCnt(40)
                .build(),

            BlogResponseDTO.builder()
                .blogId(5)
                .title("msa")
                .content("kafka")
                .email("lim")
                .viewCnt(50)
                .build()
        ));
    }

    public List<BlogResponseDTO> findByAll() {

        System.out.println(
            "debug >>>> blog dao findByAll() "
        );

        return blogs;
    }
}
  • 생성자에서 ArrayList에 미리 데이터 5개를 정의
package features.blogs.repository;

import features.blogs.domain.dto.BlogResponseDTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/*
repository, dao

- data access logic
- JDBC를 기본으로해서 Object Relationship Mapping
  (mybatis, JPA) 확장
- 원격에 있는 database와 통신을 통해서
  CRUD 작업을 전담하는 객체
*/

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class BlogRepository {

    private BlogResponseDTO[] list
        = new BlogResponseDTO[10];

    public BlogResponseDTO[] blogs() {

        System.out.println(
            "debug >>>> BlogRepository blogs() "
        );

        return list;
    }
}

4. 개념 요약

개념설명
프론트 컨트롤러요청을 받아서 그 요청이 어떤 컨트롤러에 매핑되는지 찾아서 컨트롤러에 요청을 전달, 다시 그 응답을 앱 계층에 전달
싱글톤한번에 하나의 인스턴스만 생성 → 모두 동일한 인스턴스를 공유하도록 강제 가능
컨트롤러요청을 받아 서비스에 위임하고, 서비스가 준 결과를 그대로 응답으로 반환
DTO컨트롤러와 서비스를 오가는 데이터가 담기는 그릇
• 응답 줄 때: service 처리 결과를 ResponseDTO에 담아서 view에 전달
• 요청 올 때: 사용자 입력을 RequestDTO에 담아서 service한테 전달
서비스실제 비즈니스 로직을 수행
DAO, RepositoryORM을 통해 데이터베이스를 읽고 Entity 실제 데이터를 읽어옴
읽기, 쓰기, 수정, 삭제 = CRUD 수행

5. 회고

사실 정확하게 서비스 계층의 비즈니스 로직에 오버라이드를 적용하는 구성은 이해하지 못했다.

다만 오늘은 스프링의 로직을 이해하는데 집중했다.

내일 실습을 통해서 DAO에 대해서 다시 개념을 보완해봐야겠다.


참고자료

https://ittrue.tistory.com/140

0개의 댓글