Spring from Starter to Intermediate

이정빈·2024년 4월 1일

스프링 부트

목록 보기
2/2
post-thumbnail

Spring 이 뭐야?

Spring이라는 것은 자바에서 활용하기 위한 오픈 소스 애플리케이션 프레임워크이자 제어 반전 컨테이너 입니다. Spring이라는 것은 처음 Rod Johnson에 의해서 출시 되었으며 Enterprise Edition platform 위에 웹 애플리케이션을 구축하기 위한 확장 기능이 있고 Plain Old Java Object 프로그래밍 모델을 활성화하여 자바 개발을 단순화하고 좋은 설계 관행을 촉진하도록 설계되었습니다.

🍃 Spring 🍃 의 주요 기능

  • 제어의 역전 : 객체나 프로그램의 일부분의 제어를 컨테이너나 프레임워크로 전환하는 원칙입니다. 스프링에서는 IoC가 DI(Dependency Injection)을 통해서 달성되며, 이는 애플리케이션의 다양한 구성 요소를 유연하게 결합하는 데 도움이 됩니다.
  • 관점 지향 프로그래밍(AOP) : 스프링은 로깅, 트랜젝션 관리 둥과 같은 애플리케이션의 비즈니스 로직에서 교차 관심사를 분리하기 위해 AOP를 지원합니다.
  • 데이터 접근 프레임워크 : 스프링은 JDBC, Hibernate, JPA 등과 함께 작동하는 일관된 데이터 접근 프레임워크를 제공하여 데이터베이스와 상호 작용하는 데 필요한 보일레 플레이트 코드의 양을 단순화합니다.
  • 트랜잭션 관리 : 스프링은 로컬 트랜잭션에서 엔터프라이즈 애플리케이션을 위한 글로번 트랜잭션(JTA)에 이르기까지 확장 가능한 일관된 트랜잭션 관리 인터페이스를 제공합니다.
  • MVC(Model-View-Controller) : Spring MVC는 Model-View-Controller 아키텍처와 유연하고 느슨하게 결합된 웹 애플리켕이션을 개발하기 위한 준비된 구성 요소를 제공하는 웹 프레임워크 입니다.
  • 보안 : Spring Security는 Java EE 기반 엔터프라이즈 소프트웨어 애플리케이션에 대한 포괄적인 보안 서비스를 제공합니다.

Project Set-up

현재 스프링은 3.x 버전이 나오고 있습니다. 그럼에 따라 Java 17 or 21을 사용해야합니다. 그 밖에도 복잡하게 얽혀있던 설정 부분을 아주 간편하게 spring-starter를 통해서 설정을 마칠 수 있습니다.

  • Project : Gradle-Groovy
  • SPring Boot : 3.x.x
  • Language : Java
  • Packaging : Jar
  • Java : 17 or 21
  • groupId : Hello
  • artifactId : hello-spring
  • Dependencies : Spring Web, Thymleaf

와 같이 설정을 해주면 됩니다. 우선 스프링은 크게 Gradle 과 Maven으로 나뉘게 됩니다.

Gradle vs Maven

Gradle, Maven은 Java Spring Boot를 구성할 때 가장 널리 사용되는 빌드 자동화 도구입니다. 이들은 프로젝트의 라이브러리 의존성 관리, 빌드 프로세스, 패키징 그리고 프로젝트의 다양한 라이프사이클 관리를 자동화하는 데 사용됩니다. 그래서 팀의 성격과 선호도에 따라 그리고 요구사항에 따라 선택이 될 수 있지만 현재 추세에는 Gradle이 더 많이 사용이 됩니다.

1. Maven
Maven은 XML 파일을 사용하여 프로젝트의 구조, 의존성, 빌드 과정 등을 정의합니다. 이로인해 얻을 수 있는 장점과 단점은 아래와 같습니다.

장점

  • 표준화된 빌드 프로세스를 사용하여 새로운 개발자가 프로젝트에 참여할 때 빠르게 적응하여 실전에 투입 될 수 있습니다.
  • 중앙 레포지토리 관리 시스템을 사용하기 때문에 의존성 관리를 쉽게 할 수 있으며, 필요한 라이브러리와 그 의존성들을 자동으로 다운로드 받아 관리 할 수 있습니다.
  • 플러그인 에코시스템이라고 하여 많은 플러그인을 통해 빌드 생명주기를 쉽게 커스텀할 수 있습니다.

단점

  • xml 구성을 통해 프로젝트 설정을 관리해야 하기 때문에 때로는 설정 파일이 복잡하고 길어질 수 있습니다.
  • 유연성이 부족한 Maven의 라이프 사이클 표준 구조로 인해 매우 특정한 경우의 프로젝트 구성이나 빌드 프로세스의 커스텀이 어려울 수 있습니다.

💡빌드 생명 주기 ?
Build life cycle은 Maven에 핵심 디자인 패턴이라고 할 수 있습니다. 빌드 생명 주기는 일종의 소프트 웨어 빌드 과정이 발생하는 것의 주기라고 할 수 있습니다. 이러한 주기를 메이븐은 특정한 주기로 굳혀 하나의 패턴으로 수행합니다. Verify -> Compile -> Test -> Package -> Verify -> Install -> Deploy

2. Gradle
Gradle은 Groovy 기반의 도메인 특정 언어(DSL)를 사용하여 빌드 스크립트를 작성합니다. Maven과 비교했을 때, 더 유연한 구성이 가능하며, 빌드 프로세스의 성능도 개선된 모델입니다.

장점

  • Gradle은 증분 빌드와 빌드 캐시 기능을 통해 빌드 시간을 크게 단축 시킵니다.
  • Groovy나 Kotlin을 사용하여 빌드 스크립트를 작성하기 때문에, Maven보다 훨씬 유연한 빌드 구성이 가능합니다.

단점

  • 하지만, 다양한 언어를 지원하고 유연하게 빌드를 할 수 있는 만큼 배우기가 어렵고 많은 사전 지식이 필수적입니다.

Library

  • Spring Web에는 spring-boot-starter-tomcat 서버라고 하여 웹서버를 내장하여 IntelliJ 내에서 바로 웹서버에 변경 사항을 빌드할 수 있게 도와주는 라이브러리가 내장 되어 있습니다. 그리고 spring-webmvc도 있는데, 이는 DI를 도와주고 구성하는 역할을 수행합니다.
  • spring-boot-starter-thymleaf는 내가 구성한 html을 바로 볼 수 있게 도와주는 라이브러리입니다.
  • 그 밖에도 스프링 부트의 핵심 기능인 유틸, 어노테이션을 담고 있는 spring-core와 기본적인 로깅을 제공하는 logback, slf4j가 있습니다.

Test Library

  • spring-boot-starter-test에는 JUnit이라는 테스트 프레임워크와 mockito, assertj라고 불리며 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리도 있고 마지막으로 spring-test라고 하여 스프링 통합 테스트를 지원합니다.

Spring 웹 개발 기초

정적 컨텐츠

static content는 파일 서버 시스템을 거치지 않고 브라우저에 띄워지는 것으로 스프링도 해당 기능을 제공합니다.

<!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>
<div class = "container">
  <div>
    <h1>Hello Spring!</h1>
    <p>Sign-in</p>
    <p>
      <a href = "/members/new">Sign-up</a>
      <a href = "/members">Members</a>
    </p>
  </div>
</div>
</body>
</html>

위에서 언급을 했듯이, 우리가 localhost를 통해서 html에 들어가게 되면 내장 되어 있는 톰캣 서버를 통해서 Spring container에 있는 html을 불러와서 해당 서버에 로드를 하여 우리 브라우저에 보여주게 됩니다! 이러한 과정인 static contents의 로드 과정입니다.

MVC와 Template Engine & API

<!DOCTYPE HTML>
<html>
<head>
  <title>Hello</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text = "'Hello  ' + ${name}">Hi, sir!</p>
</body>
</html>
package Hello.HelloSpring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {

    @GetMapping("hello")
    // GetMapping is like a method to load "hello"
    public String hello(Model model){
        model.addAttribute("data", "hello!!");
        return "hello";
        // Basically, if we set returns like this, IntelliJ automaically will find a file name which is same
        // with Return.
    }

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

    @GetMapping("hello-string")
    @ResponseBody
    // html의 Body 부분의 및의 return을 직접 넣는 것이다.
    // View 같은 것 없이 문자 그대로 나온다.
    public String helloString(@RequestParam("name") String name){
        return "Hello " + name;
    }

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

위의 코드는 보는 것 처럼 클래스 밖에 Controller라는 Annotation을 볼 수 있습니다. 즉 View를 제어하기 위한 클래스라는 것을 Spring에 알려주는 것이라고 할 수 있고, 클래스 내부를 뜯어 보면, GetMapping안에 각기 다른 문장들이 들어오는 것을 알 수 있습니다.

만약 localhost:8080/hello등으로 접속을 하게 되었을 때 <p th:text = "'Hello ' + ${name}">Hi, sir!</p>를 이용하여 p 태그 내부의 내용을 변경하는 것입니다. 그리고 GetMapping 아래의 @RespondBody 같은 어노테이션은 바디 부분 안에 직접적으로 내용을 넣어 보여주는 것을 말합니다.

  • @RespondBody 부연 설명
  1. HTTP의 BODY 안에 문자 내용을 직접 반환합니다.
  2. viewResolver 대신 HttpMessageConverter가 동작합니다.
  3. 기본 문자 처리 : StringHttpMessageConverter
  4. 기본 객체 처리 : MappingJackson2HttpMessageConverter
  5. byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있다.

회원 관리 예제

Diagram

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

위의 다이어그램을 이용하여 회원 ID, 이름을 등록하고 조회하는 로직을 구현해보겠습니다. 이 상황에서 아직 DB는 미정되어 언제든지 바꿔줄 수 있는 Interface의 형태로 구현하겠습니다.

Domain

package Hello.HelloSpring.Domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;    /* 데이터를 저장하기 위해서 시스템이 정하는 임의의 값*/
    private String name;

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

우리는 회원 관리 예제를 만들기 위해서 Member라는 클래스 안에 id와 name이라는 인스턴스를 생성하고 그를 각각 불러 오고 설정 할 수 있는 Getter/Setter를 선언합니다. 그리고 제일 최상단에 있는 어노테이션의 경우는 Long id가 ID 값으로 쓰이고 DB에서 데이터가 쌓일 수록 자동으로 id의 숫자가 증가하게 끔 한다는 의미를 지니고 있습니다.

Repository

package Hello.HelloSpring.repository;

import Hello.HelloSpring.Domain.Member;
import org.springframework.stereotype.Repository;

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

// 회원 객체를 저장하는 저장소
public interface MemberRepository {
    /*
    * save를 하면 저장소에 멤버라는 객체가 저장이 된다.
    * 그 다음 부터, findBy**에 의해서 각각을 불러올 수 있고
    * 전체를 findAll로 불러올 수 있다.
    */
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();


}

회원 객체를 저장하는 저장소로 MemberRepository를 만들어 db가 결정되기 전에 쓸 저장소를 만들고 그에 쓰일 메서드를 선언하되 Optional로 선언을 하여 null값도 처리 할 수 있게 합니다.

package Hello.HelloSpring.repository;

import Hello.HelloSpring.Domain.Member;
import org.springframework.stereotype.Repository;

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) {
        /*
        * Id를 셋업을 하고, 해당 값을 HashMap, store에
        * 저장하여 return을 저장한 키값인 member로 한다.
        */
        member.setId(++sequence);
        store.put(member.getId(),member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        /*
        * Store의 Id는 Null 값이 충분히 나올 수 있다.
        * 그렇기 때문에, Optioonal.ofNullable() 안에 감싸서
        * null 값이 오는 경우의 작업을 수행 할 수 있다.*/
        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() {
        // store의 Member value를 반환한다.
        return new ArrayList<>(store.values());
    }

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

위의 코드는 MemberRepository에 있는 메서드는 오버라이딩을 한 메서드들입니다. 추가적으로 알아야 할 것은 clearStore() 메서드들로 이전에 저장된 인스턴스에 의해서 나중에 오는 값이 영향이 받는 것을 방지 하기 위해 이전에 쌓여 있던 데이터를 지우는 작업을 시행합니다.

Repository Test Code

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();
    
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }
    @Test
    public void save() {
		//given
        Member member = new Member();
        member.setName("spring");
		//when
        repository.save(member);
		//then
        Member result = repository.findById(member.getId()).get();
        assertThat(result).isEqualTo(member);
    }
    @Test
    public void findByName() {
		//given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
		Member member2 = new Member();member2.setName("spring2");
        repository.save(member2);
		//when
        Member result = repository.findByName("spring1").get();
		//then
        assertThat(result).isEqualTo(member1);
    }
    @Test
    public void findAll() {
		//given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
		//when
        List<Member> result = repository.findAll();
		//then
        assertThat(result.size()).isEqualTo(2);
    }
}

Spring 개발에 있어서 Test 코드의 작성은 매우 중요합니다. 이러한 작업을 반복 실행가능하게 만들기 위해서 JUnit이라는 프레임워크를 이용합니다.

여기서 알아야 할 것은 모든 테스트는 순서에 의존하지 않고 독립적으로 실해이 됩니다. 그렇기 때문에 순서가 엇갈려서 오류가 발생하는 일을 방지하고자, 한 과정이 끝나면 Repository를 비워주는 코드를 작성한것이 @AfterEach 후에 나오는 코드라고 할 수 있습니다.

그리고 테스트를 함에 있어서 유용한 라이브러리인 Assertions가 있습니다. 해당 라이브러리를 활용하면 만약 테스트를 진행하였을 경우 올바르지 않은 결과가 도출 될 경우에 에러 메세지를 보여주고 아닐 경우에는 바로 실행 종료가 되는 라이브러리로 간단한 테스트를 진행할 때 많이 사용이 됩니다.

Member Service Test Code

package Hello.HelloSpring.service;

import Hello.HelloSpring.Domain.Member;
import Hello.HelloSpring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

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 join() {
        //given
        Member member = new Member();
        member.setName("Spring");

        //when
        Long saveId = memberService.join(member);

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

    @Test
    public void ExceptionOfOverlapped() {
        // 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("It's already existed.");
        //        try {
        //            memberService.join(member2);
        //            fail();
        //        } catch (IllegalStateException e) {
        //            assertThat(e.getMessage()).isEqualTo("It's already existed.");
        //        }

        // then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

위에서 사용한 AfterEach와는 다르게 BeforeEach는 테스트 시작전에 동작한다고 보면 됩니다. 그리고 테스트를 만들고 시험을 할 대마다 계속 객체를 만드는 것은 혼란을 가져올 수 있기 때문에 선언만 해두고 테스트의 전후로 인스턴스의 초기화 과정을 거치는 과정에서 @BeforeEach 어노테이션으로 선언된 테스트 클래스 안에 객체 생성을 하는 코드를 삽입하여 주는 것이 훨씬 명확한 코드가 될 수 있습니다.


스프링 빈과 의존관계

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

package Hello.HelloSpring.controller;

import Hello.HelloSpring.Domain.Member;
import Hello.HelloSpring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

/**
 * Controller라는 annotation을 보고서
 * Spring이라는 Container 안에 annotation을 한 부분의
 * 객체가 생성된다.
 * 이것을 Spring container에서 Spring bean이 관리된다라고 한다.
 */

@Controller
public class MemberController {
    private final MemberService memberService;


    // 생성자로 만들고 , 해당 컨테이너가 만들어질때 만드는데
    // 이 어노테이션을 통해 컨테이너에 있는 객체와 자동으로 연결한다.
    // 멤버 컨트롤러가 생성이 될 때 스프링 빈에 등록되어 있는
    // MemberService 객체를 연결해 주는 것이다.
    // Controller는 코드로 설정할 수 있는 영역이 아니기 때문에
    // 컴포넌트 스캔 방식을 이용하기 위해서, Controller&Autowired를 사용한다.


    @Autowired
    public MemberController(MemberService memberService) {
        // 생성자 주입 방식(DI의)
        this.memberService = memberService;
    }

    //    Setter 주입 방식
    //    중간에 설정이 바뀔 일이 없는데, 계속 퍼블릭으로 열려 있으면 오류가 날 수 있다.
    //    @Autowired
    //    public void setMemberService(MemberService memberService) {
    //        this.memberService = memberService;
    //    }
    // @Autowired private final MemberService memberService; -> 필드 주입 방법

    @GetMapping(value = "/members/new")
    public String createForm(){
        return "members/createForm";
    }

    @PostMapping(value = "/members/new")
    public String create(MemberForm form){

        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }

    @GetMapping(value = "/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

Spring은 Controller라는 annotation을 보고서 Spring Container 안에 객체가 생성된다. 이것을 Spring container에서 Spring bean이 관리된다라고 한다. 위의 코드는 멤버를 조회 및 등록을 하는 과정에서 화면을 넘기는 과정을 처리하기 위해서 짜여진 코드입니다. 그렇기 때문에 객체를 계속 만들기 보다는 선언을 하고 사용하는 것이 좋습니다.

생성자에 @AutoWired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어줍니다. 이렇게 객체를 넣어 주는 과정을 DI, Dependency Injection이라고 합니다.

Component scan

Component Scan의 원리는 다음과 같습니다. @Controller와 같은 어노테이션의 정의를 따라가다 보면 내부에 공통적으로 @Component, @Service, @Repository 등이 있는 것을 알 수 있습니다. 여기서 컨트롤러가 스프링 빈으로 자동 등록이 되는 것도 @Controller 내의 @Component에 의해서 동작을 하는 것입니다.

자바 코드로 직접 스프링 빈 등록하는 방법

package Hello.HelloSpring;

import Hello.HelloSpring.aop.TimeTreceAop;
import Hello.HelloSpring.repository.JpaMemberRepository;
import Hello.HelloSpring.repository.MemberRepository;
import Hello.HelloSpring.service.MemberService;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

    @Bean
    public MemberService memberService() {
        // 이렇게 끝이 아닌 MemberRepository 객체를 안에 넣어줘야 한다.
        return new MemberService(memberRepository);
    }
}

위의 코드는 자바 코드로 직접 스프링 빈을 등록하는 코드입니다. 해당 코드는 후의 DB와 연결하여 주는 코드이기 때문에 조금 다를 수 있습니다.


회원 관리 예제 - 웹 MVC 개발

홈 화면 추가

package Hello.HelloSpring.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/*
* 어노테이션은 해당 클래스가 Spring MVC 컨트롤러의 역할을 한다는 것을 Spring Framework에 알립니다.
* 컨트롤러는 일반적으로 사용자의 웹 요청을 처리하고, 모델과 뷰를 조작하여 사용자에게 응답을 반환합니다.
* @Controller가 붙은 클래스는 웹 요청을 처리하는 핸들러 메소드(handler methods)를 포함하며,
* Spring은 이 어노테이션을 사용하여 웹 요청을 처리할 적절한 메소드를 찾습니다.
 * */
@Controller
public class HomeController {

    @GetMapping("/")
    public String home(){
        return "home";
    }
}
<!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>
<div class = "container">
  <div>
    <h1>Hello Spring!</h1>
    <p>Sign-in</p>
    <p>
      <a href = "/members/new">Sign-up</a>
      <a href = "/members">Members</a>
    </p>
  </div>
</div>
</body>
</html>

가장 먼저 홈페이지에 접속을 했을 경우에 보여줄 html과 Controller를 등록하여 초기에 보여줄 화면을 설정합니다.

회원 웹 기능 - 등록 및 조회


    @GetMapping(value = "/members/new")
    public String createForm(){
        return "members/createForm";
    }

    @PostMapping(value = "/members/new")
    public String create(MemberForm form){

        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }

    @GetMapping(value = "/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
package Hello.HelloSpring.controller;

public class MemberForm {
    private String name;

    public String getName() {
        return name;
    }

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

위의 코드를 사용하여 각각의 등록 및 조회를 할때 화면의 actions을 설정하여 줍니다.


H2 DB에 연결하기

H2 DB는 빠른 속도와 쉬운 사용법에 의해서 Spring boot를 사용할때 같이 자주 쓰이는 데이터베이스 중에 하나입니다. 하지만, 실무에서는 주로 다른 것을 사용하고 h2는 로컬의 데이터베이스로 사용합니다.

스프링 JdbcTemplate

package Hello.HelloSpring.repository;

import Hello.HelloSpring.Domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import javax.sql.DataSource;
import java.util.*;

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

해당 Jdbc Template은 MyBatis 같은 라이브러리로 JDBC API에서 본 반복 코드를 대부분 제거해주는 코드입니다. 하지만, SQL은 직접 작성해야합니다. 위 같이 원래는 임의의 DB를 사용하다가 H2로 연결하여 주고 해당 내용을 처리하기 위해서 Jdbc를 사용하기 위해 SpringConfig를 아래와 같이 수정합니다.

	@Bean
    public MemberService memberService() {
        // 이렇게 끝이 아닌 MemberRepository 객체를 안에 넣어줘야 한다.
        return new JdbcTemplateMemberRepository(dataSource);
    }

이렇게 수정함으로써, 해당 데이터를 연결한게 됩니다.

JPA

JPA는 매우 광범위한 공부 내용을 가진 영역입니다. 여기서는 간단하게만 언급을 하고 넘어가겠습니다. JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전활할 수 있고, 개발 생산성을 크게 높일 수 있습니다. 이를 통해서 회원 레포지토리를 새로 구성해보겠습니다.

package Hello.HelloSpring.repository;

import Hello.HelloSpring.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;
    }

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
        }
    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class).getResultList();

    }
    @Override
    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();
    }
}

이렇게 연결 시스템을 바꿈으로써, 수동으로 sql에 쏴주었던 것을 더욱 간결하게 바꾼것을 알 수 있습니다. 그리고 이렇게 연결 db가 바뀌었기 때문에 Spring Config 파일의 MemberRepository 생성자의 return 값을 수정하여 줍니다. ➡️ return new JpaMemberRepository(em)

의존 관계를 정리 할 때 하나의 추상화된 것에 대해 의존해야하는 원칙에 의거하여 SpringDataJpaMemberRepository를 만들어 아래와 같이 파일을 생성합니다.

package Hello.HelloSpring.repository;

import Hello.HelloSpring.Domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

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

}

정의를 살펴보면 JpaRepository라는 라이브러리를 통해 우리가 원하는 두가지의 타입을 이용하여 프로그램을 구성할 수 있습니다. 여기서는 회원을 이름으로 조회하는 것을 따로 인터페이스로 만들어 구성하는 코드라고 할 수 있습니다. 또한

위의 JPA 회원 레포지토리를 사용하도록 SpringConfig를 최종적으로 수정합니다.

	@Bean
    public MemberService memberService() {
        // 이렇게 끝이 아닌 MemberRepository 객체를 안에 넣어줘야 한다.
        return new MemberService(memberRepository);
    }

결론

지금 현재의 마지막 예제는 H2 데이터 베이스와 연동을 하여 MemberService를 기존에 MemberRepository 인터페이스에 선언한 아이들을 끌어와 JpaMemeberRepository로 가져와 오버라이딩을 하여 Jdbc에서는 SQL을 직접 인젝션을 해줘야 하는 부분까지 편하게 바꿨습니다. 그리고 회원 조회하는 부분 또한 SpringDataJpaMemberRepository로 연결하여 해당 작업을 연결하여 줬습니다.

결국 간단하게 유저는 MemberService와 MemberSerivce는 Jpa를 통해 H2 db와 연결되어 있는 관계를 생각할 수 있습니다.

profile
백엔드 화이팅 :)

0개의 댓글