프로젝트 생성

스프링 프레임워크의 기능을 사용하지는 않을 거지만, 빠르게 프로젝트를 생성하기 위해 spring initializr를 통해 프로젝트를 생성한다.

조금 더 빠른 실행을 위해 IntelliJ 설정 > Gradle에서 빌드/테스트 실행 환경을 IntelliJ IDEA로 수정한다.

github remote repository 생성 후 해당 프로젝트를 연결했다.

# 프로젝트 디렉토리 루트 위치에서 실행
git init

# remote repository 설정(origin)
git remote add origin https://github.com/hongbre/spring-basic-1.git

# 사용 브랜치 master → main 변경
git branch -b main

# 변경 된 모든 파일 커밋 대상으로 추가
git add .

# 커밋 메시지와 함께 커밋
git commit -m "Project 생성"

# remote repository(github)에 push
git push origin main

비즈니스 요구사항

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원을 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용한다. (미확정)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고자 한다. 최악의 경우 할인을 적용하지 않을 수 있다. (미확정)

미확정인 회원 데이터 저장 방법, 할인 정책은 언제든지 변경 가능하도록 인터페이스로 구현할 필요가 있다.

회원 도메인 설계 및 개발

회원 도메인 클래스 다이어그램

회원 도메인 개발

회원 등급을 가진 enum 타입의 Grade와 회원 엔티티를 가진 Member 클래스를 생성한다.

public enum Grade {
    BASIC,
    VIP
}
public class Member {
    private Long id;
    private String name;
    private Grade grade;

    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    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;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}

이후 MemberRepository 인터페이스를 생성하고, MemoryMemberRepository 구현 클래스를 생성한다.

public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    ...
}

MemorymemberRepository에서 HashMap을 사용하고 있는데, 동시성 문제가 발생할 수 있으니 ConcurrentHashMap 사용을 권장한다고 한다. 이때 HashMap을 사용 할 때 동시성 문제가 발생하는 이유는 주로 여러 스레드가 동시에 맵을 수정하거나 접근할 때 발생하며, 데이터 무결성이 깨지는 문제이다. ConcurrentHashMap은 세그먼트 잠금(내부를 16개 세그먼트로 분할하여 부분적 잠금 사용), 원자적 메소드를 제공하여 복합 연상을 스레드 세이프하게 처리하는 등 스레드의 안전한 연산을 보장하고, Fail-Safe 반복자 사용으로 수정 중에도 안전하게 순회할 수 있다.

MemberService 인터페이스를 생성하고, MemberServiceImpl 구현 클래스를 생성, MemberApp를 생성해서 main 클래스를 추가해주는데 여기서 테스트를 진행하는 것은 바람직하지 못하니 JUnit 테스트 클래스를 생성해서 진행한다.

public class MemberServiceTest {
    MemberService memberService = new MemberServiceImpl();

    @Test
    void join() {
        //given
        Member member = new Member(1L, "memberA", Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}

가입한 member와 findMember로 찾은 멤버가 같으면 테스트는 성공한다.

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    ...
}

여기서 문제가 있다면 Service 구현 클래스에서 인터페이스인 MemberRepository에도 의존하고, 구현 클래스인 MemorymemberRepository에도 의존하고 있다.

주문과 할인 정책 도메인 설계 및 개발

주문과 할인 정책 도메인 클래스 다이어그램

주문과 할인 정책 도메인 개발

memberId, itemName, itemPrice, discountPrice 4개의 속성을 가진 Order 클래스를 생성하고, OrderService 인터페이스와 OrderServiceImpl 구현 클래스 생성 및 DiscountPolicy 인터페이스와 FixDiscountPolicy 구현 클래스를 생성하게 된다. 동일하게 OrderApp 이라는 main 클래스를 추가해주었고, JUnit 테스트 클래스를 생성하였다.

public class OrderServiceTest {
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder() {
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

memberA를 VIP 등급으로 회원 가입 시키고, 10000원짜리 itemA를 주문했을 때 할인 금액이 1000원이면 테스트는 성공한다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 4  (1) 2025.04.17
Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 1  (0) 2025.03.23
Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 6  (0) 2025.02.18

스터디 시작

김영한 Spring 로드맵의 두 번째인 스프링 핵심 원리 - 기본편 강의를 보며 각자의 블로그에 정리하고 이를 교환 해 서로의 지식을 공유하기로 했다.

스프링(Spring)이란?

2002년, EJB(Enterprise Java Beans)를 사용하던 개발자 중 한 명인 로드 존슨(Rod Johnson)이 EJB의 문제점을 지적한 책을 출간하였다. EJB 없이도 고품질의 확장 가능한 애플리케이션 개발이 가능함을 보여주고 있어 책이 유명해졌고, 유겐 휠러(Juergen Hoeller)와 얀 카로프(Yann Caroff)가 로드 존슨에게 오픈소스 프로젝트를 제안하며 시작되었다고 한다. 스프링의 핵심 코드의 상당수는 현재도 유겐 휠러가 개발하고 있고, 여담으로 이름의 유래는 EJB(J2EE)라는 겨울을 넘어 새로운 시작(봄)이라는 뜻으로 지었다고 한다.
 


spring.io 내 projects 화면을 켭쳐했다. 핵심이 되는 Spring Boot와 Spring Framework가 제일 위에 나오고, Spring Data, Cloud, Security 등 24개의 프로젝트(작성일 기준)가 존재한다.

Spring Boot

Spring Boot는 Spring Framework를 기반으로 빠르게 애플리케이션을 구축할 수 있는 프레임워크이다. Spring Boot는 클래스 패스와 빈 설정을 분석하여 필요한 구성 요소를 자동으로 추가하여 환경(Infrastructure) 설정에 대한 부담을 줄인다. 이를 통해 개발자는 비즈니스 로직에 집중할 수 있다. Spring Boot는 Starter Projects와 Auto-Configuration을 통해 쉽게 마이크로 서비스를 개발할 수 있도록 지원한다.

Spring Framework

Spring Framework는 Enterprise Java 애플리케이션을 구축하기 위한 오픈소스 프레임워크이다. AOP(Aspect-Oriented Programming), 스프링 DI 컨테이너(Dependency Injection) 같은 기술을 제공하여 복잡한 개발을 단순화한다. 또한 Spring Framework는 IoC(Inversion of Control) 컨테이너를 통해 Java 객체의 생명주기를 관리하고, Core 컨테이너는 Core, Beans, Context 모듈로 구성되어있다.

다른 프로젝트들

  • Spring Data : 다양한 데이터베이스에 일관된 접근 방식을 제공한다.
  • Spring Cloud : 분산 시스템에서 공통 패턴을 쉽게 구축할 수 있도록 도와준다.
  • Spring Security : 인증 및 권한 부여를 관리하며, 보안 강화를 위해 사용한다.
  • Spring Integration : 이벤트 기반 액션을 처리하는데 적합하며, 데이터 레이어 위에서 이벤트 캡쳐 및 처리가 가능하다.
     
    강의에서는 핵심 개념을 이해하는 것이 해당 기술을 이해하고 사용하는데 가장 중요하다고 얘기한다. 이 기술을 왜 만들었는지? 이 기술의 핵심 컨셉이 무엇인지? 이다. 스프링은 Java 기반의 프레임워크로, Java의 가장 큰 특징으로는 객체 지향 언어인 것이다. 따라서 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내어 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크라는 것이 스프링의 핵심 개념이다.

좋은 객체 지향 프로그래밍이란?

우선 객체 지향 프로그래밍(OOP, Object-Oriented Programming)의 특징이다.

  1. 캡슐화(Encapsulation) : 데이터와 코드의 형태를 외부로부터 숨기고, 데이터의 구조와 역할, 기능을 하나의 단위로 묶는 방법이다. 이는 데이터의 무결성을 유지하고, 객체 간의 상호 작용을 명확히 정의한다.
  2. 추상화(Abstraction) : 복잡한 시스템의 핵심적인 특성만을 드러내고, 불필요한 세부 사항은 숨기는 방법이다. 이를 통해 시스템의 복잡성을 줄이고 이해하기 쉽게 만든다.
  3. 상속(Inheritance) : 새로운 클래스가 기존 클래스의 속성과 메서드를 물려받아 사용하는 기능이다. 이를 통해 코드의 재사용성을 높이고, 계층 구조를 형성할 수 있다.
  4. 다형성(Polymorphism) : 객체가 다양한 형태로 표현될 수 있는 능력이다. 이는 오버로딩(Overloading)과 오버라이딩(Overriding)을 통해 구현된다.

위 특성들을 바탕으로 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만든다. 여기서 '유연하고 변경이 용이'는 레고를 조립하듯, 키보드를 교체하듯이 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법을 말한다. 여기서 자연스럽게 특징 중 하나인 다형성(Polymorphism)에 집중하게 된다.

다형성의 실세계 비유

물론 실세계와 객체 지향을 1:1로 매칭하는 것을 불가하다. 하지만 역할과 구현으로 세상을 구분하면 다형성을 이해하기 보다 쉽다.

운전자 역할과 자동차 역할로 구분했을 때, 자동차 구현인 차A가 자동차 역할을 하든 차Z가 자동차 역할을 하든 운전자 역할에서 봤을 때는 자동차 역할이다. 어떤 자동차가 와도 운전을 할 수 있는 것이다. 이와 같이 역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해진다. 장점으로는 클라이언트가 대상의 역할(인터페이스)만 알면 되고, 구현체의 내부 구조를 몰라도 되고, 내부 구조가 변경되어도 영향이 없으며, 구현체 자체를 변경해도 영향을 받지 않는다는 것이다.
 
Java의 다형성을 활용하면 역할은 곧 인터페이스(Interface)이며, 구현은 인터페이스를 통해 구현한 클래스나 구현 객체를 의미한다. 이는 객체를 설계할 때 역할과 구현을 명확히 분리해야 한다는 말로, 역할(인터페이스)을 먼저 부여하고 그 역할을 수행하는 구현 객체를 만들어야 한다. 혼자 있는 객체는 없으며 수많은 객체 클라이언트(요청)와 객체 서버(응답)는 서로 협력 관계를 가진다.

Java의 다형성

메서드 오버라이딩은 자식 클래스가 부모 클래스의 메서드를 동일한 이름, 매개변수, 반환 타입으로 재정의하는 것을 의미한다. 이를 통해 자식 클래스는 부모 클래스의 메서드와 같은 이름을 가지지만, 다른 구현을 제공할 수 있다. 이런 메서드 오버라이딩은 다형성을 구현하는 중요한 방법 중 하나이다.

입문 스터디 당시에 구현했던 MemberRepository이다. 이때도 MemberRepository 인터페이스를 통해 MemoryMemberRepository, JdbcMemberRepository 등 다양한 데이터 저장 방식으로 클래스를 구현했었다. MemberService가 클라이언트 역할이고 MemberRepository가 서버 역할일 때, 클라이언트를 변경하지 않고 서버의 기능을 유현하게 변경할 수 있었다.
 
이런들 저런들 역할과 구현이라는 컨셉을 다형성을 통해 객체 지향으로 구현할 수 있으며, 이는 유연하고 변경이 용이한, 확장 가능한 설계가 된다. 변경 시 클라이언트에 영향을 주지 않기 위해서는 인터페이스를 안정적으로 설계하는 것이 중요하다. 물론 인터페이스 자체가 변하게 되면 큰 변경이 발생한다는 한계도 있다.

SOLID 원칙

객체 지향 설계의 SOLID 원칙은 소프트웨어를 더 유지보수하기 쉽고 확장 가능한 구조로 만드는데 도음을 주는 다섯 가지 기본 원칙이다. SOLID는 SRP, OCP, LSP, ISP, DIP의 첫 글자를 따서 만든 약어이다.

  1. SRP(Single Responsibility Principle, 단일 책임 원칙)
    • 클래스는 하나의 책임만을 가져야 한다. 즉, 클래스는 하나의 이유로만 변경되어야 한다.
    • 하나의 책임이라는 것은 모호하나, 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 SRP 원칙을 잘 따른 것이다.
  2. OCP(Open-Closed Principle, 개방-폐쇄 원칙)
    • 소프트웨어 요소(Entity)는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 한다.
    • 다형성, 위에서 얘기한 역할과 구현의 분리를 생각할 수 있다.
    • 입문 스터디에서 구현했을 당시에는 MemberService가 구현 클래스를 직접하며 코드를 변경했었다.
      • 다형성(인터페이스를 통한 클래스 구현)은 사용 했으나 OCP 원칙은 지키지 못했다.
      • 객체를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요하며 이는 추후에 배운다.
  3. LSP(Liskov Substitution Principle, 리스코프 치환 원칙)
    • 부모 클래스의 객체는 자식 클래스의 객체로 대체될 수 있어야 한다.
    • 위 자동차를 예로 들면 엑셀은 앞으로 가라는 기능인데, 뒤로 가게 구현하면 LSP 원칙 위반이다.
      • 즉, 인터페이스로 구현한 클래스는 인터페이스에 정의된 규약은 다 지켜아한다.
  4. ISP(Interface Segregation Principle, 인터페이스 분리 원칙)
    • 클라이언트는 사용하지 않는 인터페이스에 의존하지 않아야 한다.
      • 클라이언트가 불필요한 메서드나 속성에 의존하지 않도록 하여 코드의 유지보수성과 유연성을 높이는 데 도움을 준다.
      • 하나의 범용 인터페이스를 사용하기 보다는 여러 개의 특정한 인터페이스를 사용하는 것이 더 좋다.
  5. DIP(Dependency Inversion Principle, 의존관계 역전 원칙)
    • 고수준 모듈은 저수준 모듈에 의존하지 않아야 하며, 둘 다 추상화에 의존해야 한다.
      • 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다.
      • 위에서 얘기한 구현과 역할에서 역할에 의존하라는 말이다. 차A에 의존하게 되면 자동차를 바꾸기 어렵지만, 자동차에 의존하면 쉽게 바꿀 수 있다.
    • OCP와 같이 MemberService는 인터페이스에 의존하지만, 구현 클래스도 의존하고 있다.
      • 코드 내에서 직접 구현 클래스를 선택하기 때문에 DIP 원칙 위반이다.

객체 지향의 핵심은 다형성이지만, 다형성만으로는 OCP, DIP를 지킬 수 없다.

객체 지향 설계와 스프링

스프링은 스프링 DI 컨테이너를 통해 다형성을 유지하며 OCP, DIP를 준수할 수 있다. 스프링 DI 컨테이너는 객체 간 의존성을 런타임 시에 주입하는데, 이를 통해 인터페이스를 정의하고 다양한 구현체를 주입받기 때문이다. 이렇게 스프링으로 개발하게 되면 클라이언트 코드의 변경 없이 기능 확장이 가능하게 된다.
 
물론 이론적인 내용으로 DI를 완전히 이해하기는 어렵다. 이제 실제 코드를 작성해보면서 이해해야겠다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 2  (0) 2025.03.30
Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17

AOP

AOP(Aspect-Oriented Programming)는 공통 관심 사항(Cross-Cutting Concerns)을 모듈화하여 관리한다.

회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항(Core Concerns)가 아니고, 시간을 측정하는 로직은 공통 관심 사항이다. 하지만 각 기능에 시간을 측정하는 기능을 넣을 경우 비즈니스 로직이 섞여 유지보수가 어렵고, 시간을 측정하는 기능에 수정이 발생할 때 모든 부분을 수정해야해서 불편하다.

package hello.hello_spring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TimeTraceAop {
    @Around("execution(* hello.hello_spring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        try {
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString()+ " " + timeMs +
                    "ms");
        }
    }
}

TimeTraceAop를 작성한다. @Aspect 어노테이션으로 AOP임을 나타내고, @Around 어노테이션으로 적용될 클래스를 명시한다. 스프링 빈에 올리기 위해 @Component 어노테이션을 사용한다. 이렇게 핵심 관심 사항과 공통 관심 사항을 분리할 수 있다.

AOP 적용 전에는 memberController가 memberService를 호출했다면, AOP 적용 후에는 memberController가 프록시 memberService(복제 된)를 호출하고, 프록시 memberService가 joinPoint.proceed() 메소드를 통해 실제 memberService를 호출하게 된다.


AOP를 마지막으로 스프링 입문 강의가 끝났다. 이번에 한 AOP도 그렇고 자세하게 공부할 것들이 많다...

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 2  (0) 2025.03.30
Spring 스터디2 - 1  (0) 2025.03.23
Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 4  (0) 2025.02.16

H2 데이터베이스 설치

H2 공식 홈페이지

Windows Installer를 다운로드 받아 설치한다.

H2 Console을 클릭하여 H2 데이터베이스를 실행한다.

~/test로 연결 하면 Windows 기준 USERPROFILE 경로에 test.mv.db가 생긴다.

/* DROP TABLE IF EXISTS MEMBER CASCADE; */
CREATE TABLE MEMBER
(
    ID  BIGINT GENERATED BY DEFAULT AS IDENTITY,
    NAME VARCHAR(255),
    PRIMARY KEY (ID)
);

INSERT INTO MEMBER(NAME) VALUES('member1');
INSERT INTO MEMBER(NAME) VALUES('member2');
INSERT INTO MEMBER(NAME) VALUES(666, '666member');

member 테이블 생성하고 데이터를 삽입한다. ID는 BIGINT 타입으로 지정하고, 값을 입력하지 않았을 때 GENERATED BY DEFAULT AS IDENTITY가 자동으로 값을 입력해준다. NAME은 VARCHAR 타입으로 255자까지 가변적으로 입력가능한 문자열이다. PK로 ID를 지정하여 고유한 값을 갖게 된다.

좌측 메뉴에서 테이블명을 선택하면 자동으로 Select문이 생성되어 쉽게 조회 할 수 있다. 방금 삽입한 3개의 member가 조회된다.

순수 JDBC

이제는 사용하지 않는 방식이라고 넘어갔지만 코드 작성해서 실행해보았다.

@Configuration
public class SpringConfig {

    @Autowired
    private final DataSource dataSource;

    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }

}

SpringConfig에 DataSource를 추가해주고, memberRepository에서 작성한 JdbcMemberRepository로 변경한다. DataSource는 데이터베이스와의 커넥션을 획득할 때 사용하는 객체로, 스프링 부트에서는 정보(application.properties)를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어 두기 때문에 의존성 주입(DI)을 받을 수 있다.

스프링 통합 테스트

MemberServiceIntegrationTest
package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

이전에 했던 단위 테스트 말고 통합 테스트를 위한 MemberServiceIntegrationTest 클래스 파일을 작성한다. @SpringBootTest 어노테이션으로 스프링 컨테이너를 테스트와 함께 실행하도록 하며, @Transactional 어노테이션으로 테스트 트랜잭션 시작 후 종료 시에 전부 롤백하도록 한다. @Transactional 어노테이션이 없으면 데이터베이스 PK 문제 발생 시 테스트 케이스를 계속 바꿔줘야할 수 있다.

스프링 JdbcTemplate

스프링 JdbcTemplate는 JDBC 코드의 반복적인 부분을 줄여준다.

JdbcTemplateMemberRepository
//import 생략
public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
        return result.stream().findAny();
    }
    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }
    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
        return result.stream().findAny();
    }
    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

이후에 Spring Config에서 JdbcTemplateMemberRepository를 사용하도록 수정해준다.

//생략
    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }

JPA

JPA는 자바에서 ORM(Object-Relational Mapping, 객체-관계 매핑)을 위한 표준 인터페이스로, 자바 객체와 관계형 데이터베이스 간의 매핑을 쉽게 하여 개발자가 SQL 쿼리를 작성하지 않아도 된다. (인터페이스이므로 실제 구현이 필요하다. 대표적으로 Hibernate, EclipseLink, OpenJPA 등이 있다.)

주요 기능으로는 객체 중심의 개발(쿼리보다 자바 객체에 집중하여 개발 생산성 향상과 유지보수 용이), CRUD 작업 자동화, ORM 기능(자바 객체와 데이터베이스 테이블 간의 매핑 제공으로 패러다임 불일치 해결)이 있고, 주요 구성 요소로는 Entity(데이터베이스 테이블과 매핑되는 자바 클래스, @Entity 어노테이션), Entity Manager(Entity의 생명주기를 관리하는 인터페이스로 CRUD 작업 수행), Persistent Context(영속성 컨텍스트, Entity Manager가 관리하는 Entity 인스턴스의 집합으로 데이터베이스와의 동기화를 도움)가 있다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

우선 build.gradle 파일에 jpa를 추가해준다. jpa 라이브러리 안에 jdbc를 포함하므로 기존에 사용했던 jdbc는 제외한다.

//생략
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

그리고 application.properties에 옵션을 추가해준다. show-sql은 jpa가 생성한 쿼리를 보여주고, ddl-auto는 jpa가 자동으로 테이블을 생성할지 여부이다. 이미 생성된 테이블을 사용하고 있어서 none으로 설정했지만, create로 하면 테이블을 생성해준다.

//생략
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

Member 도메인을 Entity로 사용하기 위해 @Entity 어노테이션을 설정해주고, @Id 어노테이션으로 PK를 지정, @GeneratedValue 어노테이션으로 값을 자동으로 생성해준다.

JpaMemberRepository
package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class JpaMemberRepository implements MemberRepository {
    private final EntityManager em;
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
        return result.stream().findAny();
    }
}

JpaMemberRepository를 작성한다. DataSource를 받아오는게 아니라 EntityManager를 불러와서 사용한다.

//생략
@Transactional
public class MemberService {

`MemberService'에 @Transactional 어노테이션을 달아준다. jpa의 모든 쿼리는 트랙잭션 내에서 실행되어야 한다.

//생략
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;

    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }

}

SpringConfig에서 JpaMemberRepository를 사용하도록 변경한다. EntityManager를 추가한다. 이후 테스트를 돌리거나 실행하면 정상적으로 완료된다. jpa는 이런 쿼리를 생성했다.

스프링 데이터 JPA

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

SpringDataJpaMemberRepository를 작성한다. 클래스가 아닌 인터페이스로 작성하고, JpaRepository를 상속하게 되면 스프링 실행 시 자동으로 스프링 빈에 등록되어 사용할 수 있게된다.

//생략
@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}

SpringConfig에서 MemberRepository만 의존성 주입 받으면 된다.

스프링 데이터 JPA는 JPA 기반으로 구축되었고, 기본적인 CRUD 기능도 대부분 제공한다. findByID, findByAll 등등. 우선 강의에 있어 스프링 데이터 JPA를 사용해보았으나, 우선 JPA에 대한 이해도를 높일 필요가 있어 보이고, 결국 모든 쿼리를 JPA로 처리할 수 없어서 네이티브 쿼리나 JdbcTemplate과의 조합 등의 사용도 고려해야한다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 1  (0) 2025.03.23
Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 4  (0) 2025.02.16
Spring 스터디 - 3  (0) 2025.02.12

홈 화면 추가

HomeController.java와 home.html을 각각 생성한다.

HomeController.java
...
@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }
}
home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <div class="container">
    <h1>Hello Spring</h1>
    <p>회원 기능</p>
    <p>
      <a href="/members/new">회원 가입</a>
      <a href="/members">회원 목록</a>
    </p>
  </div>
</body>
</html>

빌드하고 localhost:8080 접속하면 위 화면이 나온다. 지난 번에 static/index.html 파일을 만들어서 Controller에 루트 컨텍스트("/")가 없으면 해당 파일을 보여주도록 했으나, 이번에는 Controller에 루트 컨텍스트를 지정하여 template/home.html 파일을 보여준다. 두 버튼에 대해서는 아직 화면이 없어서 오류 화면으로 넘어간다.

등록 화면 추가

MemberController.java
...
    @GetMapping("members/new")
    public String createForm() {
        return "members/createMemberForm";
    }

    @PostMapping("members/new")
    public String create(MemberForm form) {
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }
...
createMemberForm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <div class="container">
    <form action="/members/new" method="post">
      <div class="form-group">
        <label for="name">이름</label>
        <input type="text" id="name" name="name" placeholder="이름을 입력하세요">
      </div>
      <button type="submit">등록</button>
    </form>
  </div>
</body>
</html>
MemberForm.java
...
public class MemberForm {
    private String name;

    public String getName() {
        return name;
    }

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

홈 화면에서 회원 가입을 클릭하면 방금 만든 등록 화면(template/createMemberForm.html)으로 이동한다. 이동하면 위와 같이 이름을 입력하고 등록할 수 있는 버튼이 생긴다. 여기서 form을 활용하게 되는데, form 태그에 명시한대로 등록(submit) 버튼을 클릭하면 POST 메소드를 사용해 /members/new를 호출한다. MemberController에서 @PostMapping 어노테이션을 달고 있는 메소드를 찾고, 실행한다. MemberForm에 private로 생성한 name에 input 태그에 명시한 name 프로퍼티의 값이 name으로 같을 때 해당 MemberForm 데이터로 들어가고, 이 name으로 member를 생성, 홈 화면으로 돌아간다.

조회 화면 추가

MemberController.java
...
   @GetMapping("/members")
    public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
memberList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <div class="container">
    <table>
      <thead>
      <tr>
        <th>#</th>
        <th>이름</th>
      </tr>
      </thead>
      <tbody>
      <tr th:each="member : ${members}">
        <td th:text="${member.id}"></td>
        <td th:text="${member.name}"></td>
      </tr>
      </tbody>
    </table>
  </div>
</body>
</html>

메모리에 저장하다보니 재시작 하면 목록에 아무것도 안 나온다.

등록 화면에서 회원을 등록하면 위 처럼 id와 name이 나온다. 조회 화면에 들어오면 findMembers() 메소드로 회원 목록을 조회하고, List를 Model에 추가하여 memberList.html로 전달한다. 그럼 thymeleaf가 th:each로 members List를 순회하며 member의 id와 name을 반복하여 출력하는 형태이다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 4  (0) 2025.02.16
Spring 스터디 - 3  (0) 2025.02.12
Spring 스터디 - 2  (0) 2025.02.10

컴포넌트 스캔과 자동 의존관계 설정

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

MemberController 파일을 작성하고 잘 동작하는지 실행해보면 Parameter 0 of constructor in hello.hello_spring.controller.MemberController required a bean of type 'hello.hello_spring.service.MemberService' that could not be found.라는 오류가 발생한다. @Controller 어노테이션을 달고 있는 MemberController는 스프링 컨테이너가 시작 될 때 객체를 생성하지만, MemberService는 생성하지 않아서 발생하는 오류이다. 스프링 빈에 등록한다고 하는데, 스프링 빈에 등록하는 방법은 2가지가 있다.

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기
@Service
public class MemberService {
...
}

MemberService 앞에 @Service 어노테이션을 달면 스프링 빈에 등록된다. MemoryMemberRepository도 마찬가지고 @Repository 어노테이션을 등록하면 된다. 원래는 @Component 어노테이션이 스캔 대상이지만, @Controller, @Service, @Repository 어노테이션 안에도 @Component 어노테이션을 포함하고 있어서 스캔 대상(스프링 빈에 등록)에 포함된다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

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

MemberService 생성자에 @Autowired 어노테이션을 달면 객체 생성 시점에 스프링 컨테이너가 스프링 빈에서 내용을 찾아 의존성을 주입한다. 참고로 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다. 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다. 설정을 통해 싱글톤이 아니게 할 수 있으나, 특별한 경우를 제외 대부분 싱글톤을 사용한다.
 
싱글톤 패턴
 
클래스의 인스턴스를 하나만 생성하고, 이를 전역으로 공유하여 사용하는 디자인 패턴

  • 특징 : 인스턴스의 유일성 보장, 전역 접근 가능, 메모리 효율성(장점과 유사)
  • 단점 : 높은 결합도, 멀티스레드 문제, 테스트 어려움

스프링 빈
 
스프링 컨테이너에 의해 관리되는 Java 객체를 의미한다. 일반적인 Java 객체와 동일하지만, 스프링 컨테이너가 객체의 생성, 초기화, 의존성 주입, 소멸 등 생명 주기를 관리한다는 점에서 차이가 있다.

자바 코드로 직접 스프링 빈 등록하기

MemberService와 MemoryMemberRepository에 등록한 @Service, @Repository, @Autowired 어노테이션 제거 후 SpringConfig 클래스 작성한다.

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

@Configuration 어노테이션으로 시작하고, 각 스프링 빈에 추가할 객체에 대해서 @Bean 어노테이션을 달고 작성해준다. 이전에 어떤 형태로 데이터를 저장할지 정해진게 없어 Repository는 인터페이스 형태로 추가하였고, 현재는 MemoryMemberRepository를 사용하고 있다. 나중에 DB나 다른 형태로 변경 될 때 SpringConfig에서 내용만 바꿔주면 된다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 3  (0) 2025.02.12
Spring 스터디 - 2  (0) 2025.02.10
Spring 스터디 - 1  (0) 2025.02.04

비즈니스 요구사항 정리

데이터는 회원ID와 이름, 기능은 회원 등록과 조회만 있고 아직 DB가 선정되지 않은 가상의 시나리오이다.

일반적인 웹 애플리케이션의 계층 구조이다.

  • Controller : 웹 MVC의 컨트롤러 역할을 한다.
  • Service : 핵심 비즈니스 로직을 구현한다.
  • Repository : DB에 접근하며, 도메인 객체를 DB에 저장하고 관리한다.
  • Domain : 비즈니스 도메인 객체(예 : 회원, 주문, 쿠폰 등)로 주로 DB에 저장하고 관리한다.

클래스 의존 관계이다. 아직 DB가 선정되지 않아 인터페이스로 구현 클래스를 변경할 수 있도록 설계하며, DB는 RDB, NoSQL 등 다양한 저장소를 고민 중인 상황이다. 개발 진행을 위해 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용한다. 여기서 인터페이스는 '클래스가 따라야 할 설계도' 역할로 다음과 같은 특징을 가진다.

  • 구성 요소
    • 추상 메소드 : 인터페이스에 선언된 메소드는 기본적으로 abstract이며, 구현 클래스에서 반드시 구현해야 한다.
    • 상수 : 인터페이스에 선언된 변수는 자동으로 public static final이 된다.
    • 기타 메소드 : Java 8 부터 default 메소드와 static 메소드를 포함할 수 있다.
  • 다중 상속 지원 : 클래스는 하나의 부모 클래스만 상속할 수 있지만, 인터페이는 여러개를 구현할 수 있다.
  • 객체 생성 불가 : 인터페이스 자체로는 객체를 생성할 수 없으며, 이를 구현한 클래스만 인스턴스화가 가능하다.
  • 유연한 참조 : 인터페이스 타입의 참조변수를 통해 구현 객체를 다룰 수 있어, 코드 변경 없이 다양한 객체를 교체할 수 있다.

회원 도메인과 리포지토리 만들기

Member.java
package hello.hello_spring.domain;

public class Member {

    private Long id;
    private String name;

    public String getName() {
        return name;
    }

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}
MemberRepository.java
package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

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

public interface MemberRepository {
    //저장
    Member save(Member member);
    //Id로 찾기
    Optional<Member> findById(Long id);
    //이름으로 찾기
    Optional<Member> findByName(String name);
    //모든 회원 찾기
    List<Member> findAll();
}
MemoryMemberRepository.java
package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

회원 리포지토리 테스트 케이스 작성

MemoryMemberRepositoryTest.java
public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        Member result = repository.findById(member.getId()).get();
        assertThat(member).isEqualTo(result);
    }

    @Test
    public void findByName() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring1");
        repository.save(member2);

        Member result = repository.findByName("spring1").get();

        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring1");
        repository.save(member2);

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);
    }
}

JUnit을 활용한 테스트이다. main 디렉토리 아래가 아닌 test 디렉토리 아래에 테스트를 위한 클래스를 작성하면 된다. 테스트 메소드가 여러개 있을 때 코드에 작성된 대로 순차적으로 실행되지 않기 때문에 변수 등에 저장된 값에 의해 의도치 않은 실패가 발생할 수 있다. 이를 방지하기 위해 @AfterEach 어노테이션을 통해 각 테스트 메소드가 종료되면 실행하는 메소드를 작성하면 된다. 여기서는 HashMap을 clear한다.

테스트는 언제나 중요하며, 개발 → 테스트가 아닌 반대의 경우도 있다. 이를 테스트 주도 개발(Test-Driven Development, TDD)라고 한다. TDD는 테스트 코드를 먼저 작성한 후 이를 통과하는 실제 코드를 구현하는 방식이다.코드 품질 향상, 유지보수 용이성 등의 장점이 있으면서 초기 시간 투자 증가, 학습 곡선 등의 단점도 있다.

회원 서비스 개발

MemberService.java
public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    //회원 가입
    public Long join(Member member) {
        //중복 회원 불가
        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);
    }
}

서비스는 실제 비즈니스 로직을 구현한다.

회원 서비스 테스트

클래스에 대해 테스트 클래스를 쉽게 생성하는 방법은 윈도우 기준 Ctrl + Shift + T 단축키를 누르면 된다.

MemberService.java
class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("hello");
        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void 중복회원가입() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        IllegalStateException e = Assertions.assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        /*
        try {
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
        */
    }

    @Test
    void 전체회원조회() {
        Member member1 = new Member();
        member1.setName("spring1");
        memberRepository.save(member1);

        Member member2 = new Member();
        member2.setName("spring1");
        memberRepository.save(member2);

        List<Member> result = memberRepository.findAll();

        assertThat(result.size()).isEqualTo(2);
    }

    @Test
    void 회원조회() {
        Member member1 = new Member();
        member1.setName("spring1");
        memberRepository.save(member1);

        Member result = memberRepository.findByName("spring1").get();

        assertThat(result).isEqualTo(member1);
    }
}

테스트를 작성할 때는 given(어떤 환경에서), when(어떤 조건이 발생했을때), then(어떤 결과가 나오는지)의 3단계로 작성하면 쉽다. @BeforeEach 어노테이션으로 테스트 시에도 같은 MemberRepository를 사용하기 위해 의존성 주입 형태로 변경했다. 의존성 주입(Dependency Injection, DI)은 객체 간의 의존 관계를 외부에서 주입하여 관리하는 설계 패턴이다. 의존성 주입에 대해서는 다음에 자세히 알아보겠다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 4  (0) 2025.02.16
Spring 스터디 - 2  (0) 2025.02.10
Spring 스터디 - 1  (0) 2025.02.04

정적 컨텐츠

정적 컨텐츠(Static Content)는 기본적으로 /static 경로 아래에 있는 파일을 그대로 보여준다.

 

index.html을 호출하면 먼저 Controller에서 매핑된 게 있는지 확인하고, 없으면 /static에서 확인한다.
정정 컨텐츠는 프로그램(계산한 값을 가진다거나 등) 할 수 없다.

MVC와 템플릿 엔진

MVC는 Model, View, Controller의 각 앞 글자를 딴 것이다.

  • Controller
//Controller
@Controller
public class HelloController {

    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello");
        return "hello";
    }
}
  • View
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Hello</title>
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}">안녕하세요. 손님</p>
</body>
</html>

View는 보여지는 역할이며, Model이나 Controller는 내부에서 비즈니스 로직을 처리하는 역할이다.

    @GetMapping("hello-mvc")
    public String helloMvc(@RequestParam("name") String name, Model model) {
        model.addAttribute("name", name);
        return "hello-template";
    }

/hello-mvc?name=hong을 호출하면 Controller에서 hello-mvc와 매핑된 걸 찾고, 입력 받은 name 파라미터를 받아서 hello-template로 View에전달하면, View는 ${name}을 파라미터로 치환한다. 여기서 템플릿 엔진인 Thymeleaf가 템플릿 양식과 데이터를 결합한 최종 HTML을 생성해서 클라이언트에 전달하는 방식이다.

API

@GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name) {
        Hello hello = new Hello();
        hello.setName(name);
        return hello;
    }

    static class Hello {
        private String name;

        public String getName() {
            return name;
        }

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

@ResponseBody 어노테이션을 사용하면 위 템플릿 엔진과 다르게 HTTP Body에 JSON 형식의 데이터가 그대로 전달된다. /hello-api?name=hong을 호출하면 Controller에서 hello-api와 매핑된 걸 찾고, @ResponseBody를 확인하면 viewResolver 대신 HttpMessageConverter가 동작하면서 JSON 형식으로 전달하는 것이다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 4  (0) 2025.02.16
Spring 스터디 - 3  (0) 2025.02.12
Spring 스터디 - 1  (0) 2025.02.04

스터디 시작

김영한 Spring 로드맵의 첫 번째인 스프링 입문 강의를 보며 각자의 블로그에 정리하고 이를 교환 해 서로의 지식을 공유하기로 했다.

Java 및 IDE(IntelliJ) 설치

Java 21 설치

https://www.oracle.com/kr/java/technologies/downloads/#jdk21-windows

오라클 회원가입 하고 x64 Installer로 다운로드 후 설치 진행
(환경 변수 설정까지 해주면 좋다.)

IntelliJ 설치

https://www.jetbrains.com/idea/download/?section=windows
위에는 유료 버전이라 더 많은 기능을 쓸 수 있고, 학생 인증 등 특정 조건 충족하면 할인이 있다.
살짝 내리면 무료 버전인 Community Edition이 있어 이걸로 다운로드 후 설치 진행

Spring Boot 프로젝트 생성

https://start.spring.io
Spring Boot 프로젝트 생성을 도와준다.

  • Project
    • Gradle(Groovy), Gradle(Kotlin), Maven
      • Gradle(Groovy) : 빌드 스크립트를 Groovy로 작성하며, 간결하고 유연한 구문. 기본 설정.
      • Gradle(Kotlin) : 빌드 스크립트를 Kotlin으로 작성하며, 정적 타입 검사와 IDE 지원의 강력함.
      • Maven : 빌드 스크립트를 XML로 작성하며, 명확한 구조와 표준화된 설정 방식. 의존성 관리와 플러그인 구성이 직관적.
  • Language
    • Java, Kotlin, Groovy
  • Spring Boot
    • SNAPTSHOT은 개발 버전, M은 마일스톤 버전으로 둘 다 정식 릴리즈 전 버전이므로 괄호가 없는 버전으로 선택.
  • Project Metadata
    • Group : 프로젝트의 그룹 ID. 일반적으로 조직이나 도메인 이름을 역순으로 작성. 패키지 네임스페이스와 관련.
    • Artifact : 생성될 프로젝트의 이름 또는 빌드 파일의 기본 이름.
    • Name : 프로젝트의 이름. 기본적으로 Artifact와 동일.
    • Description : 프로젝트에 대한 간단한 설명. 메타데이터로만 사용.
    • Package name : Java/Kotlin 패키지 이름. 기본적으로 Group + Artifact 조합.
    • Packaging : Jar (독립 실행형 애플리케이션) | War (웹 애플리케이션)
    • Java : 23 | 21 | 17
  • Dependencies
    • 프로젝트에서 사용할 라이브러리나 Spring 관련 모듈 선택으로, 여기서 설정하면 필요한 의존성을 자동으로 추가해서 호환성 보장과 초기 개발 시간을 단축할 수 있다. 강의에서 선택한 내용은 Spring WebThymeleaf 두 가지다.

설정을 마치고 하단의 Generate 버튼을 클릭하면 프로젝트가 자동으로 다운로드된다.

Spring Boot 프로젝트 실행

'열기'로 방금 다운로드 된 프로젝트 폴더를 연다.

9번째 라인에서 main 메소드의 빌드 버튼을 누르거나, 윈도우 기준 Shift + F10로 프로젝트 빌드를 누른다.

에러 발생. 찾아보니 프로젝트에 한글이 있어서 발생했고, 영어로 바꿔줬다.

정상적으로 실행됐다. Spring에 내장된 Tomcat이 8080 포트로 웹 애플리케이션을 실행한다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 4  (0) 2025.02.16
Spring 스터디 - 3  (0) 2025.02.12
Spring 스터디 - 2  (0) 2025.02.10

+ Recent posts