Spring 기초 정리본

2022. 11. 6. 00:08강의 정리/Spring 기초

반응형

 

Spring 기술 그 자체에 매몰되지말자!
어떻게 기술을 사용해야 하는지 초점을 두자!

 

 

 

 

프로젝트 환경설정

 

먼저 스프링 부트 스타터 사이트로 이동해서 스프링 프로젝트 생성을 한다.


https://start.spring.io

 

  • Gradle Project : Life Cycle을 도와주는 Tool
  • Group : 기업 도메인 명
  • Artifact : 빌드된 결과물
  • Dependencies : 어떤 라이브러리를 사용할 것인지
    • Spring Web
    • Thymeleaf : 템플릿 엔진 

 

  • Spring Boot 버전은 SNAPSHOT, M1 같은 미정식 버전을 제외하고 최신 버전을 사용하시면 됩니다

 

Gradle 전체 설정

plugins {
	id 'org.springframework.boot' version '2.7.4'
	id 'io.spring.dependency-management' version '1.0.14.RELEASE'
	id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11' // 자바 11버젼

repositories {
	mavenCentral() // 라이브러리들을 다운받을 특정 사이트
}

dependencies { // Spring은 의존 관계의 라이브러리들을 모두 땡겨온다. Gradle에서 확인 가능
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // spring io에서 선택했던 라이브러리들
	implementation 'org.springframework.boot:spring-boot-starter-web' // spring io에서 선택했던 라이브러리들
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test' // 기본적으로 포함되는 라이브러리
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // jdbc 또한 포함
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

tasks.named('test') {
	useJUnitPlatform()
}
  • 동작 확인
    • src : 자바 파일은 src에 있다고 생각하기
    • 기본 메인 클래스 실행 : src/main/java/hello.hellospring/HelloSpringApplication
    • 스프링 부트 메인 실행 후 에러 페이지로 간단하게 동작 확인 (http://localhost:8080)

 

 

IntelliJ Gradle 대신에 자바 직접 실행

 

최근 IntelliJ 버전은 Gradle을 통해서 실행 하는 것이 기본 설정이다. 그러나 이렇게 하면 실행 속도가 느리기에 IntelliJ로 실행시키는 것이 실행 속도가 더 빠르다.

 

File - Setting 

      

 

 

라이브러리 확인하기

  • Gradle은 의존관계가 있는 라이브러리를 함께 다운로드한다.

Spring boot 라이브러리

  • spring-boot-starter-tomcat: 톰캣 (웹서버)
  • spring-webmvc: 스프링 웹 MVC
  • spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
  • spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
    • spring-boot
      • spring-core
    • spring-boot-starter-logging
      • logback, slf4j

 

테스트 라이브러리

  • spring-boot-starter-test
    • junit: 테스트 프레임워크
    • mockito: 목 라이브러리
    • assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
    • spring-test: 스프링 통합 테스트 지원

 

 

Spring은 너무 거대하기에, 필요한 것을 찾는 능력이 매우 중요하다!

 

 


 

스프링 웹 개발 기초

 

웹 페이지를 개발하는 3가지 방법

  1. 정적 컨텐츠
  2. MVC와 템플릿 엔진
  3. API

 

 

 

정적 컨텐츠

html, css를 통해 정적으로 개발하는 방법 → html 파일 이름 그 자체로 서버로 올라간다.

 

 

 

MVC와 템플릿 엔진

 

 

View : 화면을 그리는데 집중

위치 : resources/templates/hello.html

<html>
<head xmlns:th="https://www.thymeleaf.org"> <!- thymeleaf 문법을 사용하겠다.-->
<body>
Hello
<p th:text="'안녕하세요. ' + ${data}">안녕하세요. 손님</p> <!- thymeleaf 문법을 사용하고, Controller(=Views.py)에서 보낸 인자 출력  -->
</body>
</head>
</html>

 

Controller : 비즈니스 로직을 처리하는데 집중

위치 : hello.hellospring/controller

@Controller // controller는 반드시 Annotation을 해주어야 함
public class HelloController {

    @GetMapping("hello") // HTTP의 Get을 의미
    // /hello url로 들어오면 해당 함수가 매핑이 된다.
    public String hello(Model model) {
        model.addAttribute("data", "hello!"); // data라는 key 값에 hello!라는 value 전송
        return "hello"; // resources:templates 폴더 밑에 있는 "hello" 이름을 가진 템플릿을 Render
    }
  }
  • Controller에서 리턴 값으로 문자를 반환하면 뷰 리졸버(viewResolver)가 화면을 찾아서 처리 함
    • Spring boot 템플릿엔진 기본 viewName 매핑
    • resource:templates/ {viewName} + .html
      1. 컨트롤러에서 "hello" 반환
      2. 뷰 리졸버에서 resource/templates 디렉토리 아래의 hello.html을 찾아 매핑 시켜줌.

 

 

빌드하고 실행하기

  1. 콘솔로 이동 → cmd로 이동하기
  2. ./gradlew → gradlew.bat를 실행한다.
  3. cmd에서 gradlew.bat를 실행하려면 gladlew를 타이핑하고, 엔터를 친다.
  4. gradlew build

 

 

 

  	@Controller // controller는 반드시 Annotation을 해주어야 함
	public class HelloController {
  	@GetMapping("hello-mvc")
        public String helloMvc(@RequestParam(value = "name", required = false) String name, Model model) { // 외부에서 파라미터, 인자를 받고자 할 때, @RequestParam 사용
            model.addAttribute("name", name);
            return "hello-templates";
        }
    }
 

[스프링] @RequestParam에 대한 이해 - parameter를 쉽게 컨트롤하기

스프링에서는 컨트롤러로 사용할 클래스 상단에 @Controller 지정하며 유용하게 사용할수 있는 기능들이 있다. 그중에 @RequestParam에 대해서 알아볼까합니다. 1. 단일 파라미터 변환 private ModelAndView r

heavenly-appear.tistory.com

 

인자로 데이터를 받는 MVC 패턴 프로세스

  1. url에서 인자를 받음
  2. 해당 url에 매핑된 컨트롤러 실행
  3. 모델에 인자값 저장 후 템플릿 Render
  4. View에서 모델값(=인자로 받은 값) 사용

 

 

 

API

API를 보내는 방법들 

 

1. @ResponseBody 문자 반환 

    @GetMapping("hello-string") // ResponseBody 문자 반환
    @ResponseBody
    public String helloString(@RequestParam("name") String name) {
        return "hello" + name;
    }
  • 문자 반환은 자주 쓰이지 않음
  • @ResponseBody를 사용하면 뷰 리졸버를 사용하지 않음 → 즉 HTML을 렌더링하는게 아니라 API를 응답해줌
  • 대신 HTTP의 Body에 문자 내용을 직접 반환해준다.

 

 

2. @ResponseBody 객체 반환

    @GetMapping("hello-api") // ResponseBody 객체 반환
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name) { // url 인자로 넘겨받은 값을 String 형의 name 변수에 저장
        Hello hello = new Hello(); // 객체 생성
        hello.setName(name); // 객체 메소드를 통해 변수 지정
        return hello; // 객체(=Json 반환)
    }
    
     static class Hello { // static class는 상위 클래스와 독립적으로 유지되게 도와줌
        private String name; // private이기에 getter와 setter를 통해 외부에서 접근 가능케 함. = Property 접근 방식

        public String getName(){ // getter와 setter 단축기 : ALT + ENTER
            return name;
        }

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

 

  • 대부분 이 방법을 사용한다.
  • @ResponseBody를 사용하고, 객체를 반환하면 객체가 JSON으로 변환된다.

 

 

@ResponseBody의 사용 원리

  • HTTP의 BODY에 문자 내용, 객체를 직접 반환
  • 중요 !!! : viewResolver 대신에 HttpMessageConverter 가 동작
    • 기본 문자처리 : StringHttpMessageConverter
    • 기본 객체처리 : MappingJackson2HttpMessageConveter
    • 커스터마이징 가능하지만 거의 그대로 사용한다
  • byte 처리 등등 기타 여러 HttpMessageConveter가 기본으로 등록 되어있음
  • 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 HTTPMessageConverter가 선택된다.  

 

 


예제를 통해 알아보는 Spring boot 백엔드 개발

 

일반적인 개발 순서

  1. 비즈니스 요구사항 정리
  2. 회원 도메인과 리포지토리 만들기 → 저장소
  3. 회원 리포지토리 테스트 케이스 작성
  4. 회원 서비스 개발 → 실제 비지니스 로직
  5. 회원 서비스 테스트 → Junit

 

일반적인 Spring boot 개발 순서

  1. Domain 생성
  2. Repository 생성
  3. Service 생성
  4. Controller 생성

 

 

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

 

  • 컨트롤러 : 웹 MVC의 컨트롤러 역할
    • Url과 매핑된 메소드에서 유저의 요청에 응답해 서비스에서 만든 로직을 이용해 객체, 데이터를 전달해주는 역할
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
public class MemberController { // @Controller를 사용하면, Spring Container에서 스스로 MemberController 객체를 생성해서 Spring에서 저장하고,관리한다.
                                // 이를 Spring Container에서 bin이 관리된다고 표현한다.

    // private final MemberService memberService = new MemberService();
    // 이런식으로 new를 사용해 객체를 생성하면, 각 컨트롤러마다 각자 new 객체를 생성할 수 있기 때문에 
    // Spring Container로부터 등록한 후 받아서 사용할 수 있게 해야한다.

    private final MemberService memberService;

    // 의존성이 생성자를 통해서 들어오는 걸 생성자 주입이라고 한다.
    // 생성자 주입을 사용하는 것을 추천
    @Autowired // 생성자에 @Autowired가 있으면, Spring bin에 등록되어 있는 memberService 객체를 가져와 연결 시켜줌 -> Dependency Injection
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    /*
     @AutoWired private final MemberService memberService; // 필드 주입 방법 -> 중간에 변경할 수 없기에 비추

    @Autowired // setter 주입 방법 -> 누군가가 호출했을 때 public으로 되어있어야 하기에, 보안 문제 생김
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
     */

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

    @PostMapping("/members/new")
    public String create(MemberForm form) { // MemberForm과 연결시켜줌
        Member member = new Member();
        member.setName(form.getName()); // form에서 입력받은 이름을 member domain에서 객체로 생성

        memberService.join(member); // 생성한 객체를 회원가입 로직에 넘김

        return "redirect:/"; // Django의 redirect와 같은데, 홈 화면으로 바로 보냄
    }

    @GetMapping("/members")
    public String List(Model model) {
        List<Member> members = memberService.findMembers(); // 모든 멤버 객체들을 다 가져온다.
        model.addAttribute("members", members); // members라는 key 값에 members라는 value 전송
        return "members/memberList";
    }
}

 

 

  • 서비스 : 핵심 비즈니스 로직 구현
    • 리포지토리의 데이터를 이용해 비즈니스 로직들을 구현한다.
    • Service에서는 기획자도 알 수 있도록 비즈니스에 가까운 네이밍을 지어야한다!

 

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

// Testing을 쉽게 하는 방법
// 1. Ctrl + Shift + T -> Create New Test -> Junit으로 선택하고, 테스팅할 메소드 선택

//@Service  @Service는 MemberService를 Spring에 등록시켜주는 역할을 한다. -> Django에서 view, model 상속받는 것과 같다 생각하기
public class MemberService {

    // Repository 객체 생성
    // MemberService가 메모리 회원 리포지토리를 직접 생성하게 했습니다.
    // 인터페이스 사용법 : 인터페이스로 구현 객체를 사용하려면 다음과 같이 인터페이스 변수를 선언하고 구현 객체를 대입해야 합니다. 인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지를 저장합니다.
    // 인터페이스 변수 = new 구현객체;
    private final MemberRepository memberRepository; // 객체 변수에 final로 선언하면 그 변수에 다른 참조 값을 지정할 수 없습니다. 원시 타입과 동일하게 한번 쓰여진 변수는 재변경 불가합니다. 단, 객체 자체가 immutable하다는 의미는 아닙니다. 객체의 속성은 변경 가능합니다

    //@Autowired // 생성자에 @Autowired가 있으면, Spring bin에 등록되어 있는 MemberRepository 객체를 가져와 연결 시켜줌 -> Dependency Injection
    // MemberService - 연결 -> MemberRepository
    public MemberService(MemberRepository memberRepository) { // MemberService 입장에서 외부에서 객체를 넘겨주는 것을 Dependency Injection이라고 한다.
        // 3. 2단계에서 넘겨줬던 인자를 받아서 memberRepository로 저장
        // 4. 이렇게 하면 테스팅 Repository와 Service Repository가 같아진다.
        this.memberRepository = memberRepository;
    }

    // 회원가입 기능
    public long join(Member member) {
        validateDuplicateMember(member);// 중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }
    
    // 중복 회원 검증 기능
    private void validateDuplicateMember(Member member) { // ctrl + t -> Refactory와 관련된 단축기
        memberRepository.findByName(member.getName())
                .ifPresent(m -> { // member.getName()이 Null이 아니면, 즉 만약 값이 있으면 예외처리를 해라
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }
    
    // 전체 회원 조회
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    // 1명의 유저 반환
    public Optional<Member> findOne(long memberId){
        return memberRepository.findById(memberId);
    }
}

 

 

  • 리포지토리 : 데이터베이스에 접근하여서, 도메인 객체를 DB에 저장하고 관리
    • 리포지토리는 인터페이스와 인터페이스 구현체로 만들어진다.
    • 아래 코드는 JPA 방식 리포지토리 구현체
package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

@Transactional // JPA를 사용할 때는 항상 @Transactional을 어노테이션 해야한다.
public class JPAMemberRepo implements MemberRepository{

    private final EntityManager em; // JPA는 EntityManager를 통해서 모든 동작을 한다.
    // JPA는 내부적으로 Datasource를 다 탑재하고있다.

    public JPAMemberRepo(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        em.persist(member); // .persist()는 이 메서드가 호출되면 article은 managed(영속)상태로 진입하게 됩니다.
        /*
        persist 는 새로운 entity 를 영속성 컨텍스트내에서 관리하고 싶을 때 사용합니다.
        entityManager.persist(xxx) 를 호출하면 바로 insert 쿼리가 동작합니다.
         */
        return member;

    }

    @Override
    // pk 같은 경우 .find로 매핑한다.
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id); // 첫 인자로는 조회할 타입, 두번째 인자로는 식별자를 넣어주면 된다.
        // .find는 실제로 DB에서 값을 찾아온다. 추가로 Optional로 한번 감싸서 값을 가져오는데, 얘는 해당 ID의 객체가 없다면 Null을 반환한다.
        return Optional.ofNullable(member);
    }

    @Override
    // pk가 아닌 경우, jpql이라는 객체 지향 쿼리를 사용한다.
    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(); // 결과를 컬렉션으로 반환한다. 결과가 없으면 빈 컬렉션이 반환된다. 1건이면 1건만 들어간 컬렉션이 반환된다

        return result.stream().findAny();
    }

    @Override
    // pk가 아닌 경우, jpql이라는 객체 지향 쿼리를 사용한다.
    public List<Member> findAll() {  // ctrl + alt + N = Inline(하나의 문장으로 합치기)
        return em.createQuery("select m from Member m", Member.class) // Member(=m) Enitity 자체를 Select 해, 두번째 인자는 타입을 적어준다.
                // jpql은 객체를 대상으로 쿼리를 보내고, 객체를 .
                .getResultList();
    }
}

 

 

 

 

  • 도메인 : 비즈니스 도메인 객체 ex) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
    • 아래 Member Class를 하나의 DB Table이라 생각하면 편하다.
package hello.hellospring.domain;

import javax.persistence.*;

// JPA는 자바의 표준 ORM 인터페이스고 여러 구현체들이 있다.
@Entity // 이렇게 @Entity 어노테이션을 하면, JPA가 관리하는 엔티티가 되는거다.
public class Member {

    @Id // pk값으로 매핑시켜준다.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 알아서 value를 생성해주는 걸 IDENTITY라고 한다.
    private Long id; // 데이터를 구분하기 위해서 시스템에서 저장한 아이디

    @Column(name = "username") // DB에 있는 username과 아래 name 변수를 매핑시켜준다.
    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;
    }
}

 

 

 

클래스들간의 의존관계

 

 

 

Repository 테스트 케이스 작성 방법

  • 위치 : src/test/java 하위 폴더에 생성한다.
  • 이러한 Test를 먼저 만들고, 리포지토리를 만드는 것을 TDD라고 한다.
package hello.hellospring.repositiory;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository(); // 테스트할 Repository의 객체를 생성한다.

    @AfterEach // 매우 중요!!! @AfterEach는 메소드가 끝날때마다 실행되는 콜백 함수
    // 즉 save()가 실행되고, 호출 또 findByName()가 호출되고 실행
    public void afterEach() {
        repository.clearStore(); 
    }

    @Test // @Test 어노테이션을 사용하면, 해당 메소드를 바로 테스팅 실행할 수 있다.
    public void save() {
        // 테스트할 기능에 넣어줄 값들을 입력한다.
        Member member = new Member();
        member.setName("Spring");

        // repository 객체의 save 메소드에 값을 넘겨서 저장시킨다. = DB 역할
        repository.save(member);

        // save시 ID를 부여하였으므로, 잘 부여되었는지 확인한다.
        Member result = repository.findById(member.getId()).get(); // Optional에서는 get()으로 값을 꺼낼 수 있다.
        // 위 프로세스
        // 1. domain에 있는 프로퍼티(id, name)를 Member 객체를 통해 지정
        // 2. Repository의 객체를 통해서 save() 메소드 호출 -> DB에 저장
        // 3. Member 새로운 객체에 2단계의 객체를 이용해 findById() 메소드로 멤버 객체의 Id값을 가진 객체를 저장합니다.

        assertThat(result).isEqualTo(member); // 1~2단계에서 멤버 객체에 저장했던 것과, DB에 있는 것이 같은지 검증
        // assertThat은 첫 인자가 isEqualTo 인자와 같은지를 반환해줌
        // 실무에서는 해당 테스트를 통과 못하면 다음 단계로 아예 못넘어가게 막아버림
    }

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

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

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

        assertThat(result).isEqualTo(member1); // findByName으로 인자로 넘겼던 Name의 멤버 객체를 잘 불러왔는지 검사
    }

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

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

        List<Member> result = repository.findAll(); // 여러 Member 객체들을 findAll()로 모두 불러와서 Member 객체 자료형 리스트에 저장함
        // List<자료형> 리스트 명 = new ArrayList(or LinkedList)<자료형(생략가능)>();

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

}
  • 한번에 여러 테스트를 실행하면, DB에 직전 테스트의 결과가 남을 수 있다. 위의 @AfterEach 이러한 문제를 해결해주기 위해 존재하는데, @AfterEach 각 테스트가 종료될때 마다 이 기능을 실행한다. 
  • 테스트는 각각 독립적으로 실행되어야 하며, 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다!!
  • @BeforeEach는 각 테스트 실행전에 호출되며, 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존 관계도 새로 맺어준다.
  • @SpringBootTest :  스프링 컨테이너와 테스트를 함께 실행하도록 하는 어노테이션 = 통합 테스트
  • @Transactional : 테스트 케이스에 이 어노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

 

 

 

 


 

 

스프링 빈과 의존관계

 

스프링(Spring) 컨테이너가 관리하는 자바 객체를 빈(Bean)이라 한다.

더보기

제어의 역전이란, 간단히 말해서 객체의 생성 및 제어권을 사용자가 아닌 스프링에게 맡기는 것이다. 지금까지는 사용자가 new연산을 통해 객체를 생성하고 메소드를 호출했다. IoC가 적용된 경우에는 이러한 객체의 생성과 사용자의 제어권을 스프링에게 넘긴다. 사용자는 직접 new를 이용해 생성한 객체를 사용하지 않고, 스프링에 의하여 관리당하는 자바 객체를 사용한다. 이 객체를 '빈(bean)'이라 한다.

 

빈 등록하는 방법은 2가지가 있다.

  1. 컴포넌트 스캔과 자동 의존관계 설정
  2. 자바 코드로 직접 스프링 빈 등록하기

 

 

컴포넌트 스캔과 자동 의존관계 설정을 통한 빈 등록

  • @Component 어노테이션이 있으면 스프링 빈으로 자동 등록된다.
  • @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유는 컴포넌트 스캔 때문이다..
  • @Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.
    • @Controller
    • @Service
    • @Repository

 

@Service  @Service는 MemberService를 Spring에 등록시켜주는 역할을 한다. -> Django에서 view, model 상속받는 것과 같다 생각하기
public class MemberService {

    // Repository 객체 생성
    // MemberService가 메모리 회원 리포지토리를 직접 생성하게 했습니다.
    // 인터페이스 사용법 : 인터페이스로 구현 객체를 사용하려면 다음과 같이 인터페이스 변수를 선언하고 구현 객체를 대입해야 합니다. 인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지를 저장합니다.
    // 인터페이스 변수 = new 구현객체;
    private final MemberRepository memberRepository; // 객체 변수에 final로 선언하면 그 변수에 다른 참조 값을 지정할 수 없습니다. 원시 타입과 동일하게 한번 쓰여진 변수는 재변경 불가합니다. 단, 객체 자체가 immutable하다는 의미는 아닙니다. 객체의 속성은 변경 가능합니다

    @Autowired // 생성자에 @Autowired가 있으면, Spring bin에 등록되어 있는 MemberRepository 객체를 가져와 연결 시켜줌 -> Dependency Injection
    // MemberService - 연결 -> MemberRepository
    public MemberService(MemberRepository memberRepository) { // MemberService 입장에서 외부에서 객체를 넘겨주는 것을 Dependency Injection이라고 한다.
        // 3. 2단계에서 넘겨줬던 인자를 받아서 memberRepository로 저장
        // 4. 이렇게 하면 테스팅 Repository와 Service Repository가 같아진다.
        this.memberRepository = memberRepository;
    }
  • 생성자에 @Autowired를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다. 생성자가 1개만 있으면 @Autowired는 생략할 수 있다.
  • 생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. 이렇게
    객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다.
    • 객체를 스프링이 직접 생성해주니까 우리는 연관 관계를 스프링에게 반드시 알려주어야 한다!!

 

 

스프링 빈 등록 이미지

  • 스프링 빈 등록은 컴포넌트 스캔 방법에서 @Controller, @Service, @Repository를 통해 등록하고
  • 의존 관계는 아래와 같이 연결해준다.

 

컨트롤러 → 서비스 의존관계 연결

@Controller
public class MemberController { // @Controller를 사용하면, Spring Container에서 스스로 MemberController 객체를 생성해서 Spring에서 저장하고,관리한다.
                                // 이를 Spring Container에서 bin이 관리된다고 표현한다.

    // private final MemberService memberService = new MemberService();
    // 이런식으로 new를 사용해 객체를 생성하면, 각 컨트롤러마다 각자 new 객체를 생성할 수 있기 때문에 
    // Spring Container로부터 등록한 후 받아서 사용할 수 있게 해야한다.

    private final MemberService memberService;

    // 의존성이 생성자를 통해서 들어오는 걸 생성자 주입이라고 한다.
    // 생성자 주입을 사용하는 것을 추천
    @Autowired // 생성자에 @Autowired가 있으면, Spring bin에 등록되어 있는 memberService 객체를 가져와 연결 시켜줌 -> Dependency Injection
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

 

 

서비스 → 레포지토리 의존관계 연결

public class MemberService {

    // Repository 객체 생성
    // MemberService가 메모리 회원 리포지토리를 직접 생성하게 했습니다.
    // 인터페이스 사용법 : 인터페이스로 구현 객체를 사용하려면 다음과 같이 인터페이스 변수를 선언하고 구현 객체를 대입해야 합니다. 인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지를 저장합니다.
    // 인터페이스 변수 = new 구현객체;
    private final MemberRepository memberRepository; // 객체 변수에 final로 선언하면 그 변수에 다른 참조 값을 지정할 수 없습니다. 원시 타입과 동일하게 한번 쓰여진 변수는 재변경 불가합니다. 단, 객체 자체가 immutable하다는 의미는 아닙니다. 객체의 속성은 변경 가능합니다

    //@Autowired // 생성자에 @Autowired가 있으면, Spring bin에 등록되어 있는 MemberRepository 객체를 가져와 연결 시켜줌 -> Dependency Injection
    // MemberService - 연결 -> MemberRepository
    public MemberService(MemberRepository memberRepository) { // MemberService 입장에서 외부에서 객체를 넘겨주는 것을 Dependency Injection이라고 한다.
        // 3. 2단계에서 넘겨줬던 인자를 받아서 memberRepository로 저장
        // 4. 이렇게 하면 테스팅 Repository와 Service Repository가 같아진다.
        this.memberRepository = memberRepository;
    }
}

 

의존관계는 필드 주입, 세터 주입, 생성자 주입 3가지 방법이 있는데, 위와 같은 생성자 주입을 권장한다.

 

 

 

 


 

 

스프링 DB 접근 기술

 

 

Spring에서 DB 접근하기 

스프링 설정 변경 : DI

package hello.hellospring.service;

import hello.hellospring.repository.JPAMemberRepo;
import hello.hellospring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// Java Code로 직접 Spring Bean을 등록시키는 방법
// Spring이 실행될 떄 먼저 @Configuration이 있는 클래스에 들려서 @Bean이 있는 메소드들을 등록시킨다.

@Configuration
public class SpringConfig {

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

    @Autowired
    public SpringConfig(EntityManager em){
        this.em = em;
    }

    private final MemberRepository memberRepository;

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


    @Bean
    public MemberService memberService() { // 리턴을 memberRepository()을 함으로써, AutoWired를 사용한 것 같은 효과를 누린다.
        return new MemberService(memberRepository); //
        //  new는 클래스 타입의 인스턴스(객체)를 생성해주는 역할을 담당한다.
        // Ctrl + p를 누르면, 어떤 인자를 필요로 하는지 알 수 있다.
    }

    @Bean
    public MemberRepository memberRepository() {
//
//        //return new MemoryMemberRepository(); // 인터페이스는 new가 안되니 구현체를 리턴
//        //return new JDBCRepo(dataSource);
        return new JPAMemberRepo(em);
//    }

}
  • DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체이다. 스프링 부투는 데이터베이스 커넥션 정보를 바탕으로 Datasource를 생성하고, 스프링 빈으로 만들어준다. 그래서 DI를 받을 수 있다.
    1. application.properties에 datasource 관련 정보(=데이터베이스 정보)들을 입력해준다.
    2. SpringConfig에서 datasource를 import하고 객체를 만들어줌으로서 데이터베이스 정보를 가져온다. 

 

스프링의 DI

  • 개방-폐쇄 원칙(OCP, Open-Closed Principle)
    • 확장에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링의 DI(Dependency Injection)를 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

 

 

domain을 통해서 DB에 직접적으로 접근하는 것을 피할 수 있다!!

Domain이 따로 필요한 이유!!!

https://devlog-wjdrbs96.tistory.com/209

 

[Spring] 스프링 웹 계층이란?

이번 글에서는 스프링은 어떤 계층이 존재하는지와 계층의 역할을 무엇인지, 프로젝트시 패키지를 어떻게 나누는 것이 좋은지에 대해 정리해보려 한다. 스프링의 계층은 Presentation Layer, Business,

devlog-wjdrbs96.tistory.com

 

 

 

jdbcTemplate

  • jdbcTemplate는 쿼리를 통해서 DB에 접근한다고 생각하기
package hello.hellospring.repository;


import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;

public class JDBCRepo implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    @Autowired // 생성자가 하나만 있으면 @Autowired 생략 가능
    public JDBCRepo(DataSource dataSource) { // DataSource는 DB와 관계된 커넥션 정보를 담고있으며 빈으로 등록하여 인자로 넘겨준다. → 이 과정을 통해 Spring은 DataSource로 DB와의 연결을 획득한다.
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        return null;
    }

    @Override
    public Optional<Member> findById(Long id) {
        // ResultSet(java.sql.ResultSet)은 executeQuery(String sql)을 통해 쿼리 실행하면 ResultSet타입으로 반환을 해주어 결과값을 저장할 수 있다.
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id); // "select * from member where id = ?" 구문으로 Member 객체 자체를 반환받기 위해 RowMapper 사용
        return result.stream().findAny(); // stream()은 컬렉션에 있는 엘리먼트들을 하나씩 순회하면서 처리할 수 있는 코드 패턴이다. -> 여러 멤버 객체들중
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name); // "select * from member where id = ?" 구문으로 Member 객체 자체를 반환받기 위해 RowMapper 사용
        return result.stream().findAny(); // stream()은 컬렉션에 있는 엘리먼트들을 하나씩 순회하면서 처리할 수 있는 코드 패턴이다. -> 여러 멤버 객체들중
    }

    @Override
    public List<Member> findAll() {
        return null;
    }

    /*
    RowMapper를 사용하면, 원하는 형태의 결과값을 반환할 수 있다.
    SELECT로 나온 여러개의 값을 반환할 수 있을 뿐만 아니라,
    사용자가 원하는 형태로도 얼마든지 받을 수 있다.
     */
    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> { // alt + enter로 lambda로 변환 가능

            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member; // result에 저장된 id, name을 추출해서 Member 객체로 반환
        };
    }
}

 

 

 

 

JPA

  • JPA는 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
  • JPA를 사용하면 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

 

1. 먼저 build.gradle에 JPA, 데이터베이스 관련 라이브러리 추가

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

 

2. 스프링 부트에 JPA 설정 추가

위치 : resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
  • show-sql : JPA가 생성하는 SQL을 출력한다.
  • ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다.

 

 

JPA 엔티티 매핑

package hello.hellospring.domain;

import javax.persistence.*;

// JPA는 자바의 표준 ORM 인터페이스고 여러 구현체들이 있다.
@Entity // 이렇게 @Entity 어노테이션을 하면, JPA가 관리하는 엔티티가 되는거다.
public class Member {

    @Id // pk값으로 매핑시켜준다.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 알아서 value를 생성해주는 걸 IDENTITY라고 한다.
    private Long id; // 데이터를 구분하기 위해서 시스템에서 저장한 아이디

    @Column(name = "username") // DB에 있는 username과 아래 name 변수를 매핑시켜준다.
    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;
    }
}

 

 

JPA 회원 레포지토리

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

@Transactional // JPA를 사용할 때는 항상 @Transactional을 어노테이션 해야한다.
public class JPAMemberRepo implements MemberRepository{

    private final EntityManager em; // JPA는 EntityManager를 통해서 모든 동작을 한다.
    // JPA는 내부적으로 Datasource를 다 탑재하고있다.

    public JPAMemberRepo(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        em.persist(member); // .persist()는 이 메서드가 호출되면 article은 managed(영속)상태로 진입하게 됩니다.
        /*
        persist 는 새로운 entity 를 영속성 컨텍스트내에서 관리하고 싶을 때 사용합니다.
        entityManager.persist(xxx) 를 호출하면 바로 insert 쿼리가 동작합니다.
         */
        return member;

    }

    @Override
    // pk 같은 경우 .find로 매핑한다.
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id); // 첫 인자로는 조회할 타입, 두번째 인자로는 식별자를 넣어주면 된다.
        // .find는 실제로 DB에서 값을 찾아온다. 추가로 Optional로 한번 감싸서 값을 가져오는데, 얘는 해당 ID의 객체가 없다면 Null을 반환한다.
        return Optional.ofNullable(member);
    }

    @Override
    // pk가 아닌 경우, jpql이라는 객체 지향 쿼리를 사용한다.
    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(); // 결과를 컬렉션으로 반환한다. 결과가 없으면 빈 컬렉션이 반환된다. 1건이면 1건만 들어간 컬렉션이 반환된다

        return result.stream().findAny();
    }

    @Override
    // pk가 아닌 경우, jpql이라는 객체 지향 쿼리를 사용한다.
    public List<Member> findAll() {  // ctrl + alt + N = Inline(하나의 문장으로 합치기)
        return em.createQuery("select m from Member m", Member.class) // Member(=m) Enitity 자체를 Select 해, 두번째 인자는 타입을 적어준다.
                // jpql은 객체를 대상으로 쿼리를 보내고, 객체를 .
                .getResultList();
    }
}

 

 

 

서비스 계층에 트랜잭션 추가

import org.springframework.transaction.annotation.Transactional

@Transactional
public class MemberService {}
  • 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.

 

 


 

스프링 데이터 JPA

  • 스프링 데이터 JPA를 사용한다면 레포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 할 수 있고, 반복 개발한 기본 CRUD 기능 또한 스프링 데이터 JPA가 모두 제공한다.
  • 스프링 데이터 JPA는 JPA를 편리하게 사용해주는 도구이기에, 스프링 데이터 JPA를 먼저 학습해야한다.

 

 

 

스프링 데이터 JPA 회원 레포지토리

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 {
 Optional<Member> findByName(String name);
}

 

 

 

Spring JPA와 데이터 JPA 

https://velog.io/@junho918/Spring-Data-Jpa-JPA..%EA%B7%B8%EB%9E%98-%EC%95%8C%EA%B2%A0%EC%96%B4..-%EA%B7%B8%EB%9E%98%EC%84%9C-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%93%B0%EB%8A%94%EB%8D%B0

 

[Spring Data Jpa] JPA..그래 알겠어.. 그래서 스프링 데이터 JPA는 어떻게 쓰는데..

김영한님의 실전 Spring Data Jpa를 수강하고 정리한 문서 입니다. 강추! 최고최고. 인프런 김영한님 실전 스프링 데이터 JPA 링크딴거는 다 모르겠고, 기록할 부분만 기록해야겠다.\-h2 데이터베이스

velog.io

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형