스파르타 스프링 2주차

목표

  1. RDBMS - 정보를 저장,읽기,변경,삭제.
  2. Spring Data JPA 사용법.
  3. REST API 만드는 방법을 익힘.

서비스는 데이터를 다룰 수 있을 때 강력해진다.

JPA - 스프링은 자바, DB는 SQL로 작동하니까 자바명령어를 SQL로 번역해줘야함. 그게바로 JPA

스프링은 스프링이 하라는 방법대로 데이터를 주고받아야 함.

현업에서는 데이터를 주고 받을 때 DTO를 반드시 이용함.

Lombok - 코드를 절약하게 해줌.

RDBMS

성능/관리 면에서 매우 고도화된 엑셀 이라고 생각하면 됨.

MySQL을 사용할 것임. 현업에서 가장많이 씀.

  • H2 : In-memory DB이다. 서버가 작동하는 동안에만 내용을 저장하고 서버가 멈추면 데이터가 모두 삭제되는 데이터 베이스임.
  • MySQL : 스프링과 궁합이 좋아서 많은 회사에서 사용함.

테이블 생성

CREATE TABLE IF NOT EXISTS courses (
    id bigint(5) NOT NULL AUTO_INCREMENT, 
    title varchar(255) NOT NULL,
    tutor varchar(255) NOT NULL,
    PRIMARY KEY (id)
);
  • CREATE TABLE : 테이블을 만들어라

  • IF NOT EXISTS courses : courses라는 테이블이 존재 안하면

  • id, title, tutor 열로 구성

  • NOT NULL : 어느 하나도 비어있으면 안됨. 여기선 id, title, tutor를 반드시 가지고 있어야 함.

  • bigint == long자료형 varchar == String자료형

  • PRIMARY KEY (id) : 기본키 : id는 데이터 행을 구분하게 해주는 유일한 값으로 만듬.

    기본키는 다른 키와 중복x, null값 x

    테이블은 기본키를 하나만 가질 수 있음. RDBMS 이론상 모든 테이블은 반드시 하나의 기본 키를 가져야 함.

  • AUTO_INCREMENT : 자동으로 id를 증가시켜서 넣음.

INSERT INTO courses (title, tutor) VALUES
    ('웹개발의 봄, Spring', '남병관'), ('웹개발 종합반', '이범규');
  • courses 테이블의 (title, tutor)속성에 값을 넣어라.
SELECT * FROM COURSES;

테이블에 있는 모든 컬럼(열)을 조회.

이런게 좀 기본?이다.

웹 서비스를 만들기 위해 SQL을 모두 배울 필요까지는 없고 배우면 좋지만..

Java명령어를 SQL로 번역해주는 Spring Data JPA를 쓰면 된다.

일단은 H2데이터베이스를 사용할건데

main > resources > application.properties에

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb

이렇게 추가해주면 웹 콘솔에서 H2데이터베이스에 담긴 데이터를 확인해볼 수 있다.

JPA

JPA는 ,SQL을 쓰지 않고 데이터를 CRUD 할 수 있도록 도와주는 번역기,.

이게 없었다면.. 자바코드 짰다가 SQL짜고, 자바하고 이런식으로 했어야 함..

근데 JPA가 있어서 개꿀임.

JPA를 사용하기 전에 Domain 과 Repository에 대해 알아야 한다.

  • Domain : MySQL에서 Table이 있고 SQL이 있는데 스프링에서는 TABLE과 1:1대응되는 녀석임.
  • Repository : SQL 역할. Repositoty코드에 쓴 코드가 JPA코드를 사용하는 것과 같음.

즉 Table == Domain, SQL == Repository

package com.sparta.week02.domain;

import lombok.NoArgsConstructor;

import javax.persistence.*;

@NoArgsConstructor // 기본생성자를 대신 생성해줍니다.
@Entity // 테이블임을 나타냅니다.
public class Course {

    @Id // ID 값, Primary Key로 사용하겠다는 뜻입니다.
    @GeneratedValue(strategy = GenerationType.AUTO) // 자동 증가 명령입니다.
    private Long id;

    @Column(nullable = false) // 컬럼 값이고 반드시 값이 존재해야 함을 나타냅니다.
    private String title;

    @Column(nullable = false)
    private String tutor;

    //id는 데이터베이스에서 사용하는거라 getter, setter 설정x
    public String getTitle() { // setter는 Repository에서 자동으로 해줌
        return this.title;
    }
    public String getTutor() {
        return this.tutor;
    }

    public Course(String title, String tutor) { //생성자
        this.title = title;
        this.tutor = tutor;
    }
}

jpa를 이용해서 테이블을 만듬.

상속

extends : 이미 만들어둔거 가져다 쓰자 라고 선언하는 것

DB의 기본은 "생성일자", "수정일자"를 필드(열)로 가지는 것이다.

근데 매번 테이블을 새로 만들 때마다 매번 생성일자나 수정일자를 새로 만들 필요는 없다. 왜냐 ? 상속이 있으니까.

@어노테이션 : 스프링에게 야 이런 역할이 있어라고 알려주는 것임.

abstract : 직접 구현이 안된다.. 상속으로만 쓸 수 있다 라고 이해하면 됨. extends (클래스명) 이런식으로만 사용 가능한거임.

JPA심화

JPA로 CRUD하는 방법을 알아보자.

CRUD - 데이터를 생성(Create), 조회(READ), 변경(Update), 삭제(Delete)하는 기능.

  • Create
repository.save(new Course("Spring", "김경섭"));

Course테이블은 id, title, tutor 열이 있는데 repository.save()메소드로 title='Spring', tutor='김경섭'을 넣어주었따.

  • Read
// 데이터 전부 조회하기
            List<Course> courseList = repository.findAll();
            for (int i = 0; i < courseList.size(); i++) {
                Course course = courseList.get(i);
                System.out.println(course.getId());
                System.out.println(course.getTitle());
                System.out.println(course.getTutor());
            }

            // 데이터 하나 조회하기
            Course course = repository.findById(1L).orElseThrow(
                    () -> new NullPointerException("id가 존재하지 않습니다.")
            );

데이터를 모두 조회하려면 repository.findAll()메소드를 통해 가능하고, 하나만 조회하려면 repository.findById()를 이용하면 된다. orElseThrow는 id값이 존재하지 않을 때에 대한 에러처리를 해준 것이다.

Update, delete로 넘어가기전에 Service의 개념을 알고 넘어가야한다.

Service의 개념

스프링 구조는 3가지 영역으로 나눌 수 있다.

  1. Controller : 요청/응답을 처리.
  2. Service : 실제 중요한 작동이 많이 일어나는 부분
  3. Repo : DB와 맞닿아 있음.

요청/응답을 처리하는 부분과 DB를 꺼내오는 부분 사이에는 Service라는 연결고리가 필요하다.

Update 같은 경우 밖에서 요청이 들어오면 Repository까지 전달을 해줘야 한다. 이때 Service를 활용하는 것이다.

update 서비스를 만드는법.

  1. Course.java에 update메소드를 만들어준다.
public void update(Course course) {
    this.title =course.title;
    this.tutor =course.tutor;
}
  1. service패키지 안에 CourseService.java를 만든다.
@Service // 스프링에게 이 클래스는 서비스임을 명시. SQL같이 업데이트 하는 작용이 일어날 수 있으니까 알아둬~
public class CourseService {

    // final: 서비스에게 꼭 필요한 녀석임을 명시. 검색,업데이트 등 이럴 때도 Repository가 필요함. SQL을 날리니까.
    private final CourseRepository courseRepository;

    // 생성자를 통해, Service 클래스를 만들 때 꼭 Repository를 넣어주도록
    // 스프링에게 알려줌
    public CourseService(CourseRepository courseRepository) {
        this.courseRepository = courseRepository;
    }

    @Transactional // SQL 쿼리가 일어나야 함을 스프링에게 알려줌
    public Long update(Long id, Course course) {
        Course course1 = courseRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다.")
        );
        course1.update(course);
        return course1.getId();
    }
}

update메소드 부분을 보면 이게 뭐냐면..

update메소드에 매개번수로 들어온 id를 db(courseRepository)에서 찾아서 course1에다가 넣어주고 course1에 아까 만들어준 update메소드를 이용해서 update메소드에 들어온 course를 넣어준거.

  • Q. 왜 update만 Service 에서 관리하나요?

Lombok, DTO

Controller와 API를 배우기 전에 알아두면 좋은 Lombok과 DTO에 대해 알아볼 것이다.

Lombok

  • Lombok : 자바 프로젝트는 반복적으로 쓰이는 코드 (ex. Getter, Setter, 생성자 등)가 많은데, 이런 코드들을 자동으로 생성해 줌으로써 코드를 절약할 수 있도록 도와주는 라이브러리.

    사용하려면 설치를 하고, 환경설정을 해야함. 설치는 처음에 하면되고, Settings창에서 (ctrl alt s)

    Annotation Processors에서 Enable annotation processing 을 체크해준다. 이거는 어노테이션을 잘 작성할 수 있게끔 인텔리제이가 나한테 지원을 해달라는거.

    그다음 File > Settings.. > Plugin > "lombok" 검색 > Install

    그 다음 코드를 줄여보자

    Course.java

    package com.sparta.week02.domain;
    
    import lombok.NoArgsConstructor;
    
    import javax.persistence.*;
    **@Getter 아래에 getter부분 지우고 이거 붙이면 됨.** 
    @NoArgsConstructor // 기본생성자를 대신 생성해줍니다. **이것도 기본생정자를 생성해준거.**
    @Entity // 테이블임을 나타냅니다.
    public class Course extends Timestamped{
    
        @Id // ID 값, Primary Key로 사용하겠다는 뜻입니다.
        @GeneratedValue(strategy = GenerationType.AUTO) // 자동 증가 명령입니다.
        private Long id;
    
        @Column(nullable = false) // 컬럼 값이고 반드시 값이 존재해야 함을 나타냅니다.
        private String title;
    
        @Column(nullable = false)
        private String tutor;
    
        public Long getId() { return this.id; }
        public String getTitle() { // setter는 Repository에서 자동으로 해줌
            return this.title;
        }
        public String getTutor() {
            return this.tutor;
        }
    
        public Course(String title, String tutor) { //생성자
            this.title = title;
            this.tutor = tutor;
        }
    
        public void update(Course course) {
            this.title = course.title;
            this.tutor = course.tutor;
        }
    }

    여기 코드에서 getter 부분을 지우고, @Getter라는 어노테이션을 붙여주면 getter를 굳이 작성하지 않아도 lombok이 자동으로 작성해준다.

    CourseService.java

    package com.sparta.week02.service;
    
    import com.sparta.week02.domain.Course;
    import com.sparta.week02.domain.CourseRepository;
    import org.springframework.stereotype.Service;
    
    import javax.transaction.Transactional;
    **@RequiredArgsConstructor 아래 생성자부분 지우고 이거 써주면 됨.**
    @Service // 스프링에게 이 클래스는 서비스임을 명시. SQL같이 업데이트 하는 작용이 일어날 수 있으니까 알아둬~
    public class CourseService {
    
        // final: 서비스에게 꼭 필요한 녀석임을 명시. 검색,업데이트 등 이럴 때도 Repository가 필요함. SQL을 날리니까.
        private final CourseRepository courseRepository;
    
        // 생성자를 통해, Service 클래스를 만들 때 꼭 Repository를 넣어주도록
        // 스프링에게 알려줌
        public CourseService(CourseRepository courseRepository) {
            this.courseRepository = courseRepository;
        }
    
        @Transactional // SQL 쿼리가 일어나야 함을 스프링에게 알려줌
        public Long update(Long id, Course course) {
            Course course1 = courseRepository.findById(id).orElseThrow(
                    () -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다.")
            );
            course1.update(course);
            return course1.getId();
        }
    }

    여기서도 CourseService라는 생성자를 지우고 @RequiredArgsConstructor 어노테이션을 붙여주면 된다.

DTO

Data Transfer Object

데이터를 주고받을 때는 새로 클래스를 만들어서 하자.. 기존에 있는 클래스 사용하지 말자 이런거

@Transactional // SQL 쿼리가 일어나야 함을 스프링에게 알려줌
    public Long update(Long id, Course course) {
        Course course1 = courseRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다.")
        );
        course1.update(course);
        return course1.getId();
    }

이렇게 업데이트를 하는 서비스에서 update 메소드를 만들었을 때 id와 course(업데이트 할 대상의 정보)를 받아왔는데.. 그 정보를 Course클래스를 이용해 전달을 받고 있다. 근데 Course 클래스를 뭔가를 전달하는 용도로 이렇게 막 이용해도 될까..?

Course클래스를 이용할 때는 직접 저장할 때나, 찾아서 클라이언트에 넘겨줄 때나 이럴 때 활용하는 거지.. 이렇게 변경할 내용을 전달하는 용도로 쓰면 좋지 않다..

왜?

Course클래스를 건드린다는 것은 DB가 변경 될 가능성이 커지는 것인데, 다른사람들이 실수로 변경을 하게된다면 시스템에 오류가 발생할 가능성이 커진다.

그러다보니 DB에 연결된 클래스는 그대로 두고, 정보를 몰고 다니는 녀석을 따로 만들 필요성이 있다. 그게 바로 DTO

DB를 업데이트 하는 경우 Course의 정보를 변경해달라고 요청을 하는 것이다. 그래서 이 DTO를 따로 만들어줘야 한다.

domain패키지에 CourseRequestDto라는 클래스를 만들어준다.

CourseRequestDto.java

package com.sparta.week02.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@RequiredArgsConstructor

public class CourseRequestDto {

    private final String title;
    private final String tutor;

//    public CourseRequestDto(String title, String tutor) { 생성자는 @RequiredArgsConstructor이거 쓰면 됨.
//        this.title = title;
//        this.tutor = tutor;
//    }

}

이렇게 만들어주고 이 클래스를 통해 이제 데이터를 변경(update)하면 된다.

API

클라이어트가 서버에 요청(Request)을 보내면, 서버가 요구사항을 처리해 응답(Response)을 반환한다.

요청은 종류에 따라 API가 여러가지가 있따. 기본적으로 4가지가 있는데...

생성(POST) / 조회(GET) / 수정(PUT) / 삭제(DELETE)

이게 REST API인데 의도를 명확히 드러내는 것이다.

API를 만드는건 요청에 응답하는 것을 만드는 것이고..이 자동응답기를 만드는 것은 controller이다. 그리고 JSON형식으로 할거니까 @RestController를 만들면 된다.

  • GET

CourseController.java

@RequiredArgsConstructor
@RestController
public class CourseController {

    private final CourseRepository courseRepository;

    @GetMapping("/api/courses")
    public List<Course> getCourses() {
        return courseRepository.findAll();
    }
}

/api/courses 주소로고 GET방식으로 요청이 오면 아래 메소드를 실행해라. 이런것임.

courseRepository에서 findAll()메소드를 실행해 db에서 정보를 전부 찾아와서 List 형태로 돌려주는 메소드임.

이런 api는 직접 브라우저에 쳐서 들어가도 되고 arc같은것들을 이용해도 된다.

  • POST

CourseController.java

// PostMapping을 통해서, 같은 주소라도 방식이 다름을 구분합니다.
    @PostMapping("/api/courses")
    public Course createCourse(@RequestBody CourseRequestDto requestDto) {
			  //@RequestBody는 요청을 받는녀석이다를 표시
        // requestDto 는, 생성 요청을 의미합니다.
        // 강의 정보를 만들기 위해서는 강의 제목과 튜터 이름이 필요하잖아요?
        // 그 정보를 가져오는 녀석입니다.

        // 저장하는 것은 Dto가 아니라 Course이니, Dto의 정보를 course에 담아야 합니다.
        // 잠시 뒤 새로운 생성자를 만듭니다.
        Course course = new Course(requestDto);

        // JPA를 이용하여 DB에 저장하고, 그 결과를 반환합니다.
        return courseRepository.save(course);
    }

POST방식으로 /api/courses 주소로 가면 아래메소드가 실행된다.

createCourse는 생성한 Course를 반환한다. 생성한 데이터를 받아야 하는데 CourseRequestDto 형태로 requestDto를 받는다.

@RequestBody는 Controller에서 요청을 받는 녀석이라는걸 표시 이게 없으면 요청한 정보가 requestDto 안에 안들어감.

스프링은 요청을 주고 받는 방식을 강제하기 때문. POST, PUT 등 데이터를 주고받을 때 API에서 넘어오는 데이터를 잘 받으려면 RequestBody 형태로 받아와야 함.

이제 body가 requestDto 안에 들어와 있으니까 새로운 Course를 만들어서 courseRepository.save() 함수를 이용해 db에 넣어줘야한다.

근데 새로운 Course를 만들 때 CourseRequestDto를 이용해서 만드는게 필요한데, 그런 생성자를 Course클래스에서 만들어줘야한다.

Course.java

public Course(CourseRequestDto requestDto) { //생성자
        this.title = requestDto.getTitle();
        this.tutor = requestDto.getTutor();
    }

Spring에서 POST를 전달하는 방법은 엄격하고 정해져있어서 암기에 가깝다.. 만든사람들이 룰을 정해놨기 때문에 그 룰을 따라야한다.

HEADERS 부분에 Content-Type : application/json 을 설정해줘야 한다. 그리고 body부분에 데이터를 넣어주면 된다.

  • PUT

CourseController.java

@PutMapping("/api/courses/{id}")
public Long updateCourse(@PathVariable Long id, @RequestBody CourseRequestDto requestDto) {
    return courseService.update(id, requestDto);
}

{id} 안에는 변형되는 값이 들어간다.

updateCourse메소드의 파라미터는 id, requestDto인데 변경할 id와 변경할 내용이다. 여기서 id 는 {id}값이 들어간다. 그래서 @PathVariable 어노테이션이 들어간다. 이게 없으면 {id}값이 id로 안들어감..

  • DELETE

CourseController.java

@DeleteMapping("/api/courses/{id}")
    public Long deleteCourse(@PathVariable Long id) {
        courseRepository.deleteById(id);
        return id;
    }

DELETE임ㅇㅇ.

좋은 웹페이지 즐겨찾기