카테고리 없음

백엔드 스터디 후기

myhousemouse 2026. 3. 8. 14:16
  • 주차/주제: 2주차 - 프로젝트 구조와 아키텍처 이해

 학습 내용 요약

MVC 구조

MVC 애플리케이션을 세 가지 주요 구성 요소로 분리한 디자인 패턴

서로 독립적으로 동작

UI와 관련된 로직 분리를 목표

Model (모델)

  • 데이터와 관련된 로직을 담당
  • DB와 연결되어 데이터를 저장하거나 가져오고, 가공하는 등의 역할
  • 스프링부트에선 보통 Entity, DTO, Repository, Service 이런 것들이 모델 영역
package hello.hello_spring.domain;

public class Member {
    private Long id;  // 회원 ID
    private String name;  // 회원 이름

    // id getter()
    public Long getId() {
        return id;
    }

    // id setter ()
    public void setId(Long id) {
        this.id = id; 
    }

    // name getter
    public String getName() {
        return name;
    }

    // name setter
    public void setName(String name) {
        this.name = name;
    }
}

Getter 메서드 : 객체의 속성(필드)의 값을 반환 (외부에서 읽을 수 있게)

  • 주로 get시작
  • 필드 이름 그대로 사용

Setter 메서드 : 객체의 속성(필드)의 값을 설정 (외부에서 수정할 수 있게)

  • 주로 set시작
  • 필드 이름 그대로 사용

View (뷰)

  • 사용자에게 보여지는 화면 (UI 해당)
  • HTML, Thymeleaf, JSON 등 사용자에게 결과를 보여주는 부분
  • 스프링부트에선 Thymeleaf, Mustache 같은 템플릿 엔진이나, REST API라면 JSON 응답이 이에 해당
    • Thymeleaf - 복잡한 화면
    • Mustache - 가볍게 사용
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello View</title>
</head>
<body>
<h1 th:text="${data}">Default Text</h1>
</body>
</html>

Controller (컨트롤러)

  • 클라이언트의 요청을 받아 적절한 Model 준비하고 View에 전달하는 역할
  • HTTP 요청을 받고, 필요한 비즈니스 로직(서비스)을 호출하고, 그 결과를 View나 JSON으로 반환
@Controller
public class HelloController {
    @GetMapping("hello") // http://localhost:8080/hello GET 요청을 보낼 때 이 메서드가 실행됨
    public String hello(Model model) { //뷰 이름을 뷰 리졸버(ViewResolver)로 뷰 파일을 찾아서 렌더링
        model.addAttribute("data", "hello!!"); //뷰(View) 가 데이터를 필요로 할 때 이 데이터를 전달
        return "hello";
    } //뷰 이름 반환, resources/templates/hello.html 을 렌더링함 (기본 템플릿이 Thymeleaf)
}

예시 (도서관에서 책을 대출하고 반납하는 과정)

1. Model (모델)

  • Book 클래스
    public class Book {
    private String title;
    private String author;
    private boolean isAvailable;  // 대출 가능 여부
    
    public Book(String title, String author) {
        this.title = title;
        this.author = author;
        this.isAvailable = true;  // 기본적으로 책은 대출 가능
    }
    
    // Getter and Setter
    public String getTitle() {
        return title;
    }
    
    public boolean isAvailable() {
        return isAvailable;
    }
    
    public void setAvailable(boolean available) {
        isAvailable = available;
    	}
    }
  • Library 클래스
public class Library {
    private List<Book> books;

    public Library() {
        books = new ArrayList<>();
        // 도서 목록 추가
        books.add(new Book("Java Programming", "John Doe"));
        books.add(new Book("Spring Framework", "Jane Doe"));
    }

    public List<Book> getBooks() {
        return books;
    }

    public boolean borrowBook(String title) {
        for (Book book : books) {
            if (book.getTitle().equals(title) && book.isAvailable()) {
                book.setAvailable(false);  // 책 대출 처리
                return true;
            }
        }
        return false;  // 책이 없거나 대출 불가능
    }

    public boolean returnBook(String title) {
        for (Book book : books) {
            if (book.getTitle().equals(title) && !book.isAvailable()) {
                book.setAvailable(true);  // 책 반납 처리
                return true;
            }
        }
        return false;  // 책이 없거나 반납할 수 없는 경우
    }
}

2. View(뷰)

book-list.html : 사용자에게 책 목록을 보여주는 페이지

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>도서관</title>
</head>
<body>
    <h1>도서 목록</h1>
    <ul>
        <li th:each="book : ${books}">
            <span th:text="${book.title}"></span> 
            <span th:text="${book.author}"></span> 
            <span th:text="${book.isAvailable ? '대출 가능' : '대출 중'}"></span>
        </li>
    </ul>
</body>
</html>

borrow-book.html: 사용자가 책을 대출할 수 있는 양식을 제공하는 페이지

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>책 대출</title>
</head>
<body>
    <h1>책을 대출합니다</h1>
    <form action="/borrow-book" method="post">
        <label for="title">책 제목:</label>
        <input type="text" id="title" name="title" />
        <button type="submit">대출하기</button>
    </form>
</body>
</html>

3. Controller (컨트롤러)

@Controller
public class LibraryController {

    private Library library = new Library();

    @GetMapping("/books")
    public String showBooks(Model model) {
        // 모델에 도서 목록 추가
        model.addAttribute("books", library.getBooks());
        return "book-list";  // book-list.html로 전달
    }

    @GetMapping("/borrow-book")
    public String borrowBookForm() {
        return "borrow-book";  // borrow-book.html로 대출 폼 전달
    }

    @PostMapping("/borrow-book")
    public String borrowBook(@RequestParam("title") String title, Model model) {
        boolean success = library.borrowBook(title);
        if (success) {
            model.addAttribute("message", "책이 대출되었습니다.");
        } else {
            model.addAttribute("message", "책을 대출할 수 없습니다.");
        }
        return "redirect:/books";  // 대출 후 책 목록으로 리다이렉트
    }
}
  • 비즈니스 로직 처리와 데이터 접근을 명확하게 분리
  • 서비스 계층(Service Layer) 중심 비즈니스 로직을 처리
  • 데이터베이스와의 상호작용 Repository에서 처리

CSR(Controller-Service-Repository) 계층 조사하기

Controller 계층 (Web Layer)

사용자의 HTTP 요청을 처리

  1. 사용자로부터 입력 데이터를 받기 (HTTP 요청 파라미터, 폼 데이터 등)
  2. Service 계층을 호출하여 비즈니스 로직 처리를 전달
  3. 그 후, 응답을 사용자에게 전달 (뷰 반환 또는 REST API 응답)

Service 계층 (Business Logic Layer)

Controller에서 받은 요청을 바탕으로 실제 비즈니스 로직을 처리

데이터베이스나 외부 API와의 상호작용을 할 때 Repository 계층을 호출

  1. 비즈니스 로직 처리: Controller에서 요청받은 데이터를 처리합니다.
  2. Repository 계층 호출: 데이터를 조회하거나 수정할 때 Repository를 호출하여 데이터를 처리합니다.
  3. 트랜잭션 처리: 서비스 계층에서 트랜잭션 관리를 담당하는 경우가 많습니다. (@Transactional 어노테이션 사용)
package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    public Long join(Member member) {
        // 같은 이름이 있는 중복 회원 X
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원");
                });
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

Repository 계층 (Data Access Layer)

Repository 데이터베이스와의 상호작용을 담당하는 계층

Spring Data JPA와 같은 ORM(Object-Relational Mapping) 프레임워크를 통해 데이터베이스 쿼리를 수행

  1. 데이터베이스 접근: Entity 객체를 데이터베이스에 저장하거나 조회하는 역할
  2. 쿼리 메서드 제공: JpaRepository를 상속하여 기본적인 CRUD 작업을 자동으로 제공하고, 맞춤형 쿼리 메서드를 정의
package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import java.util.Optional;
import java.util.List;

public interface MemberRepository {
   Member save(Member member); //저장소에 저장
   Optional<Member> findById(Long id); //조회
   Optional<Member> findByName(String name);
   List<Member> findAll();
}
  • 회원 정보 저장
  • 회원정보 id 찾기
  • 회원정보 이름 찾기
  • 전체 회원 찾기
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import java.util.*;

// MemberRepository를 구현한 MemoryMemberRepository
public class MemoryMemberRepository implements MemberRepository {

    // 메모리 저장소 (Long: id, Member: Member 객체)
    private static Map<Long, Member> store = new HashMap<>();

    // 회원 id에 대한 자동 증가 값
    private static long sequence = 0L;

    // 회원을 저장하는 메서드
    @Override
    public Member save(Member member) {
        member.setId(++sequence);  // 자동으로 증가하는 id 설정
        store.put(member.getId(), member);  // store에 회원 저장
        return member;  // 저장된 회원 객체 반환
    }

    // id로 회원을 찾는 메서드
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));  // id로 찾은 값을 Optional로 감싸서 반환
    }

    // 모든 회원을 반환하는 메서드
    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());  // store의 값을 리스트로 반환
    }

    // 이름으로 회원을 찾는 메서드
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()  // 저장된 모든 회원들에 대해
                .filter(member -> member.getName().equals(name))  // 이름이 일치하는 회원을 필터링
                .findAny();  // 일치하는 첫 번째 회원 반환 (없으면 Optional.empty() 반환)
    }

    public void clearStore() {
        store.clear();
    }
}

DTO(Data Transfer Object)의 필요성과 예시 정리하기

객체간 네트워크를 통한 데이터 전송을 목적

비즈니스 로직 포함 X

태그는 웹 브라우저에서 서버로 데이터를 전송할때 사용

ex. 게시판에 글 쓰고 전송 버튼 → 글이 서버로 전송

태그에 컨트롤러가 객체에 담아 받음 → 이 객체가 DTO → DB에 저장

→ DTO를 엔티티로 변환

  1. 데이터 전송 최적화: 필요한 데이터만 선택적으로 전송 (네트워크 서버 리소스를 절약)
  2. 계층 간 데이터 격리: 데이터베이스 모델(엔티티) 클라이언트 모델(뷰) 분리 ,계층 간의 의존성을 줄이고, 변경 사항이 데이터베이스 모델에만 영향을 미치게 할 수 있음
  3. 보안과 캡슐화:민감한 데이터를 제외한 정보만을 전송
  4. API 응답 구조 정의:API 설계 시, 데이터 교환 형식을 명확히 정의함. API 응답 구조 표준화하고, 일관성을 유지
  5. 성능 향상: 데이터만 선택적으로 전달함으로써 네트워크 비용 메모리 사용을 절약(메모리와 CPU 리소스, 데이터 처리 시간를 절약)
public class MemberDTO {
    private Long id;  // 회원 ID
    private String name;  // 회원 이름

    // 기본 생성자
    public MemberDTO() {}

    // 생성자 (필드를 통해 초기화)
    public MemberDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // Getter & Setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MemberDTO{id=" + id + ", name='" + name + "'}";
    }
}

Model과 DTO의 차이점

  • Model → 비즈니스 객체로서 사용될 가능성 있음
  • DTO는 데이터만 담고, 어떤 비즈니스 로직을 처리하는 데 사용되지 않아야함.

의존성 주입(DI)의 개념과 Spring에서의 구현 방법 학습하기

DI : Dependency Injection, 외부에서 만들어진 객체를 필요한 곳으로 가져오는 기법

스프링에서 의존성 주입을 컨테이너(IOC Container)를 사용하여 자동으로 관리

IOC((Inversion of Control) 자동으로 관리

  • 객체의 생성
  • 생명주기 관리
  • 의존성 관리

DI 필요성

  1. 결합도 낮추기: 객체가 다른 객체에 의존하는 방식을 유연하게 바꿀 수 있음
  2. 코드의 유연성 향상:객체를 유연하게 확장
  3. 유지보수 용이성: 객체의 변경이나 확장 자주 수정할 필요 없이 DI 컨테이너만 수정
  4. 테스트 용이성: 테스트용 객체(목 Mock)를 주입할 수 있어 단위 테스트가능

DI 종류

생성자 주입 (Constructor Injection): 의존성 객체를 생성자를 통해 주입

public class UserService {

    private final UserRepository userRepository;

    // 생성자에서 의존성 주입
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void performService() {
        // userRepository를 사용한 비즈니스 로직
    }
}
private final MemberRepository memberRepository = new
 MemoryMemberRepository();
 
 /* 의존성 주입 예시 - 회원가입 서비스 적용 */

private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

세터 주입 (Setter Injection): 의존성 객체를 세터 메서드를 통해 주입

public class UserService {

    private UserRepository userRepository;

    // 세터 메서드를 통해 의존성 주입
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void performService() {
        // userRepository를 사용한 비즈니스 로직
    }
}

필드 주입 (Field Injection): 의존성 객체를 필드에 직접 주입, @Autowired 어노테이션으로 의존성 주입을 할수있음

public class UserService {

    @Autowired
    private UserRepository userRepository;

    public void performService() {
        // userRepository를 사용한 비즈니스 로직
    }
}
/* 회원 컨트롤러에 의존관계 추가 */ 

@Controller
 public class MemberController {
     private final MemberService memberService;
     @Autowired
     public MemberController(MemberService memberService) {
         this.memberService = memberService;
     }
}
package hello.hello_spring.service;

import org.springframework.stereotype.Service;

@Service
public class MemberService {
    // 서비스 로직 구현
}

💡 새롭게 알게 된 점 & 어려웠던 부분

  • 새롭게 알게된 점:
    MVC 구조와 CSR(Controller-Service-Repository) 계층을 통해 각 계층의 역할을 명확히 이해할 수 있게됨.
    특히, 서비스에서 예외 처리를 하는 것이 적절하다는 점과 아키텍처의 역할과 책임을 명확히 구분하는 것의 중요성을 알게됨.
    또한, 의존성 주입(DI)에 대해 다양한 주입 방법(생성자 주입, 세터 주입, 필드 주입)을 배우고, 각각의 장단점과 언제 적합한지에 대해 실습을 진행함.
  • 어려웠던 부분:
    의존성 주입(DI)에 대한 개념을 처음 접할 때 어려움을 느낌.
    DI가 다양한 형태로 사용되고, 상황에 따라 적합한 주입 방법을 선택해야 하는 점이 복잡하게 느껴짐.
    또한, 서비스와 컨트롤러 간의 책임 구분이 모호할 때가 많아 어떤 위치에서 예외 처리를 해야 할지 고민
  • 해결 방법:
    강의와 서적을 참고하면서 실습을 진행함.
    생성자 주입, 세터 주입, 필드 주입을 직접 사용해보며 각 방식의 특성을 파악함.
    예외 처리에 대해선 서비스 계층에서 처리하는 것이 적합함을 토론을 통해 의논함.
    이러한 실습과 정보를 참고하여 점차 개념이 확립되어감.