Spring

[Spring] Spring boot, Jpa를 사용해 간단한 REST API 만들기 - (1)

jalha 2019. 10. 27. 18:28

 

https://gaebal-goebal.tistory.com/51

 

[Spring] Spring boot, Jpa를 사용해 간단한 REST API 만들기 - (0)

https://gaebal-goebal.tistory.com/40 [Docker]aws의 Docker에서 웹개발하기 - MariaDB설치, phpMyAdmin설치 환경 aws ec2의 os는 linux 2 ami docker설치되어있음 MariaDB설치 docker mariadb 이미지 다운받기 d..

gaebal-goebal.tistory.com

앞서 DB테이블을 만들어뒀다.

 

 

이제 Spring boot프로젝트를 생성해보자

 

 

좌측의 Package Exploerer에서 우클릭을 한다.

New > Other를 클릭한다.

 

 

 

 

Spring Boot 의 Spring Starter Project를 선택한다.

 

 

Type 은 빌드 방식으로 maven과 gradle로 선택할 수 있다. 필자는 gradle을 선택했다.

Name은 프로젝트명이다.

Group은 프로젝트를 생성한 조직 또는 그룹명으로 보통, URL의 역순으로 지정한다.
Package는 패키지명을 입력한다.

packaging은 jar파일로 빌드하고 싶다면 jar로, war파일로 빌드하고 싶다면 war로 변경하면 된다.

 

 

프로젝트를 생성하면 아래와 같은 구조를 볼 수 있다.

 

 

 

 

여기까지 간단한 spring boot프로젝트가 생성된것을 알 수 있다.

 

다음으로는 build.gradle에 디펜던시를 추가하고 예제파일들을 작성해보자

아래와 같이 파일구조를 만든다.

 

 

 

 

in build.gradle ..

 

plugins {
	id 'org.springframework.boot' version '2.2.0.RELEASE'
	id 'io.spring.dependency-management' version '1.0.8.RELEASE'
	id 'java'
	id 'war'
}

group = 'baedalWebsite'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
	
	// war빌드
	providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'	
	
	 // DB 연동
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.4.0'
    compile group: 'com.zaxxer', name: 'HikariCP', version: '3.3.0'
    
    // @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode 한꺼번에 설정할 수 있는 @Data도 있는 라이브러리
	providedCompile group: 'org.projectlombok', name: 'lombok', version: '1.18.10'
    
}

test {
	useJUnitPlatform()
}

 

 

in JpaConfig.java ..

 

package com.baedal.masisseo.test.config;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableTransactionManagement
// 어노테이션을 작성하는 것으로, 지정된 패키지를 검색하고 @Repository를 붙인 클래스를 Bean으로 등록
@EnableJpaRepositories( basePackages = "com.baedal.masisseo.test.repository", 
			transactionManagerRef = "mariaDB_transactionManager",
			entityManagerFactoryRef = "mariaDB_entityManagerFactory" ) 
public class JpaConfig {

    // DataSource: Connection pool을 관리하는 목적으로 사용되는 객체. 
    // DB와 관계된 connection 정보를 담고 있으며, bean으로 등록하여 인자로 넘겨준다. 이 과정을 통해 Spring은 DataSource로 DB와의 연결을 획득한다.
    // 일정량의 Connection을 미리 생성시켜 저장소에 저장했다가 프로그램에서 요청이 있으면 저장소에서 Connection 꺼내 제공한다면 시간을 절약할 수 있다. 이러한 프로그래밍 기법을 Connection Pooling이라 한다.
    @Primary
    @Bean(name = "maria_dataSource")
    // 설정 파일에서 읽어온 값을 해당 클래스에 바인딩할 것
    // application.yml의 spring.data.maria(url,className,usename,password)를 읽어온다.
    @ConfigurationProperties("spring.data.maria") 
    public DataSource mariaDataSource() { 
	return DataSourceBuilder.create().type(HikariDataSource.class).build(); 
    }

    // LocalContainerEntityManagerFactoryBean: EntityManagerFactoryBean을 Spring에서 사용하기 위한 클래스
    // SessionFactoryBean과 동일한 역할을 맡음. SessionFactoryBean과 동일하게 DataSource와 Hibernate Property, Entity가 위치한 package를 지정한다.
    // Hibernate 기반으로 동작하는 것을 지정해야하는 JpaVendor를 설정해줘야 된다.
    // Vendor:
    //  SQL은 다음과 같이 표준 SQL인 ANSI SQL이 있으며, ANSI SQL 이외에 각 DBMS Vendor(벤더, 공급업체)인 MS-SQL, Oracle, MySQL, PostgreSQL 에서 자신만의 기능을 추가한 SQL이 있다. 
    //  JPA는 어플리케이션이 직접 JDBC 레벨에서 SQL을 작성하는 것이 아닌 JPA가 직접 SQL을 작성하고 실행하는 형태
    //  제품(어플리케이션)을 개발하는데 있어서, 각각 DBMS 벤더별로 다른 모듈을 개발해 주어야 함 
    //   >> 만약 고객의 요구에 따라 오라클 DB를 기준으로 작성했던 게시판 프로그램을 MS-SQL에 맞게 추가적으로 개발하려면 엄청난 비용이 들어감
    //  개발자는 JPA를 이용함에 있어서 쿼리를 작성할 필요도 없고, JPA를 사용하더라도 각 DBMS별로 조금씩 다른 SQL 방언을 걱정할 필요도 없음
    //   >> Dialect라는 추상화된 방언 클래스를 제공하고 각 벤더에 맞는 구현체를 제공하고 있기 때문임
    //  JPA에서는 설정을 통해 원하는 Dialect만 설정해주면 해당 Dialect를 참고하여 그에 맞는 쿼리를 생성함. 변경해야 할 경우 설정만 변경해 줌으로써 불필요한 변경에 대한 자원 소모를 줄일 수 있음.
    @Primary
    @Bean(name = "mariaDB_entityManagerFactory") 
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
	    EntityManagerFactoryBuilder builder,
	    @Qualifier("maria_dataSource") 
	    DataSource dataSource) { 
	Map<String, String> map = new HashMap<>(); 
	map.put("hibernate.ejb.naming_strategy","org.hibernate.cfg.ImprovedNamingStrategy"); 
	map.put("hibernate.dialect","org.hibernate.dialect.MySQL5InnoDBDialect"); 

	return builder.dataSource(dataSource)
		.packages("com.baedal.masisseo.test.model")
		.properties(map) 
		.build(); 
    }

    // PlatformTransactionManager를 구현한 Hibernate기반  TransactionManager를 등록. 
    // 이는 Spring에서 @EnableTransactionManager와 같이 사용되어 @Transactional annotation을 사용할 수 있게 됨
    // 클래스, 메서드위에 @Transactional 이 추가되면, 이 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성
    // 이 프록시 객체는 @Transactional이 포함된 메소드가 호출 될 경우, PlatformTransactionManager를 사용하여 트랜잭션을 시작하고, 정상 여부에 따라 Commit 또는 Rollback 함
    // DataSource와 EntityManagerFactoryBean에서 생성되는 EntityManagerFactory를 지정하는 Bean임
    // 트랜잭션을 관리
    @Primary
    @Bean(name = "mariaDB_transactionManager") 
    public PlatformTransactionManager transactionManager(
	    @Qualifier("mariaDB_entityManagerFactory") 
	    EntityManagerFactory entityManagerFactory) { 
	JpaTransactionManager transactionManager = new JpaTransactionManager();
	transactionManager.setEntityManagerFactory(entityManagerFactory); 

	return transactionManager; 
    }
}

 

in TeacherController.java ..

 

package com.baedal.masisseo.test.controller;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.baedal.masisseo.test.model.Teacher;
import com.baedal.masisseo.test.repository.TeacherRepository;

// 메서드 (클래스)와 경로를 매핑
@RequestMapping("/teacher")
// 기존의 MVC @Controller와는 HTTP Response Body가 생성되는 방식의 차이가 있다. 
// 기존의 MVC @Controller는 View 기술을 사용하지만, @RestController는 객체를 반환할때 객체 데이터는 바로 JSON/XML 타입의 HTTP 응답을 직접 리턴하게 된다.
@RestController
public class TeacherController {

    // 의존성 주입을 할 때 Field Injection보다는 Constructor Injection을 추천한다.
    // 생성자의 파라미터를 통해 의존관계를 한눈에 파악할 수 있고 리팩토링의 필요성을 얻을 수 있다. 
    // >> Constructor의 파라미터가 많아짐과 동시에 하나의 클래스가 많은 책임을 떠안는다는 걸 알게된다. 
    // 필드 주입일때에 null 값이 되는게 가능하기 때문에 NullPointerException 발생 위험이 있는데, 생성자 주입은 Final 변수를 사용하기 때문에 null 이 불가능하다.
    /*
    // 필드 주입
    @Autowired
    private TeacherRepository teacherRepository;
    */
    // 생성자주입
    private final TeacherRepository teacherRepository;
    @Autowired
    public TeacherController(TeacherRepository teacherRepository) {
        this.teacherRepository = teacherRepository;
    }

    // CREATE
    // 사용자 이름을 입력받아 새로운 Teacher를 생성하고 그 결과를 반환
    // @RequestMapping(method=RequestMethod.POST) = @PostMapping
    @PostMapping("/insert")
    // @RequestBody: HTTP 요청의 body 부분을 그대로 변수에 넣음. XML, JSON 일떄 이것을 주로 사용. /teacher/insert ///body>{"name":"yue"}
    public Teacher put(@RequestBody Map<String, String> map) {
	return teacherRepository.save(new Teacher(map.get("name")));
    }

    // READ
    // 모든 사용자 리스트를 반환
    @GetMapping("/select")
    public Iterable<Teacher> list() {
	return teacherRepository.findAll();

    }
    // READ
    // 해당 ID의 사용자를 반환
    @GetMapping("/select/{id}")
    // @PathVariable: 중괄호에 명시된 값을 변수로 받음. /teacher/select/1
    public Teacher findOne(@PathVariable Long id) {
	return teacherRepository.findById(id).orElse(null);
    }

    // UPDATE
    // 해당 ID의 사용자 이름을 갱신한 뒤 그 결과를 반환
    @PutMapping("/update/{id}")
    public Teacher update(@PathVariable Long id, @RequestBody Map<String, String> map) {
	Teacher teacher = teacherRepository.findById(id).orElse(null);
	teacher.setName(map.get("name"));

	return teacherRepository.save(teacher);
    }

    // DELETE
    // 해당 ID의 사용자를 삭제
    @DeleteMapping("/delete")
    // @RequestParam: http 요청 파라미터를 변수로 받음. /teacher/delete?id=3
    public void delete(@RequestParam Long id) {
	teacherRepository.deleteById(id);
    }

}

 

in Teacher.java ..

 

package com.baedal.masisseo.test.model;


import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

// 이 엔티티가 어떤 테이블과 매핑될 것인지를 나타내는 어노테이션이며 기본적으로 클래스명과 같은 DBMS 테이블과 매핑됨. 
// 다른 테이블과 매핑할 시에는 name=[테이블명] 으로 바꿀 수 있음.
@Table(name = "teacher")
// Getter,Setter: xx필드에 선언하면 getXx(),isXx(),setXx() 메소드를 생성
// RequiredArgsConstructor: final이나 @NonNull인 필드 값만 파라미터로 받는 생성자 생성
// ToString: (exclude="xx")식으로 특정 필드를 toString()결과에서 제외시킬수있음
//           클래스명(필드1명=필드1값,필드2명=필드2값,...)식으로 출력됨
// EqualsAndHashCode: equals,hashCode메소드 자동 생성
//                    (callSuper = true)설정을 통해 부모 클래스 필드 값들도 동일한지 체크하며, 기본은 false로 자신 클래스의 필드 값들만 고려함
// 위 다섯개 애노테이션을 한꺼번에 설정해주는 애노테이션
@Data
// 다른 생성자를 생성하는 애노테이션을 사용할경우 자동으로 Data의 @RequiredArgsConstructor가 적용되지않기 때문에 명시적으로 추가해야함
@RequiredArgsConstructor
// 모든 필드 값을 파라미터로 받는 생성자 생성
@AllArgsConstructor
// 파라미터가 없는 기본 생성자 생성
@NoArgsConstructor
// JPA가 Entity로서 관리하겠다는 것을 의미. 다른 Entity와 충돌이 우려될 경우 name속성을 통해 바꿀 수 있음.
@Entity
public class Teacher {

    // 일반적으로 키(primary key)를 가지는 변수에 선언.
    @Id
    // 해당 Id 값을 어떻게 자동으로 생성할지 전략을 선택할 수 있음.
    // AUTO(default): JPA 구현체가 자동으로 생성 전략을 결정
    // IDENTITY:      기본키 생성을 데이터베이스에 위임. 예를 들어 MySQL의 경우 AUTO_INCREMENT를 사용하여 기본키를 생성 
    // SEQUENCE:      데이터베이스의 특별한 오브젝트 시퀀스를 사용하여 기본키를 생성 
    // TABLE:         데이터베이스에 키 생성 전용 테이블을 하나 만들고 이를 사용하여 기본키를 생성
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // 멤버 변수가 테이블 컬럼과 매핑됨을 나타내며 기본적으로 @Entity의 모든 멤버 변수는 @Column 어노테이션이 부착된 형태로 제공됨.
    // 필드명과 테이블명이다를경우 name속성을 통해 맞추면됨.
    @Column
    private Long id;

    // 변수에 들어온 값이 Null인지 아닌지를 판단해 주며, 만약 Null이 넘어올 경우 NullPointerException을 발생시킴.
    @NonNull
    private String name;
}

 

in TeacherRepository.java ..

 

package com.baedal.masisseo.test.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.baedal.masisseo.test.model.Teacher;

// JpaRepository<Entity, Id> 인터페이스
// 타입은 <엔티티 클래스 타입, 엔티티의 @Id 필드의 타입>
// 위 인터페이스를 사용하면 기본적인 CRUD 기능을 포함한 내장 API를 사용
// @Repository가 없어도 빈으로 등록
public interface TeacherRepository extends JpaRepository<Teacher, Long>{

}


/*
save():    레코드 저장 (insert, update)
findOne(): primary key로 레코드 한건 찾기
findAll(): 전체 레코드 불러오기. 정렬(sort), 페이징(pageable) 가능
count():   레코드 갯수
delete():  레코드 삭제
이외에도 findByXx등 다양하게 사용할 수 있다. (JpaRepository API를 통해 볼수있음)
 */

 

in Application.java ..

 

package com.baedal.masisseo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;


// @SpringBootConfiguration, @ComponentScan, @EnableAutoConfiguration을 포함하는 애노테이션
// SpringBootConfiguration: Configuration애노테이션 상위
// ComponentScan: @Component, @Configuration, @Repository, @Controller, @Service, @RestController 애노테이션을 포함한 모든 클래스를 Bean으로 등록
// EnableAutoConfiguration: spring.factories 파일에 자동으로 가져올 빈들이 정의되어있으며, 해당 빈들을 가져와 등록. AOP와 같은 각종 스프링들의 설정들이 기본으로 잡혀있는것을 찾을 수 있을 것임.
@SpringBootApplication
// Spring Boot는 jar파일로 run이 되기 때문에 외부 톰캣을 사용해서 run하기 위해서는 war파일로 변경 후 SpringBootServletInitializer를 사용해 톰캣과 연결해야함.
public class BaedalWebApplication extends SpringBootServletInitializer {
    
    // war로 패키징하기위해 추가한 메소드
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
	return application.sources(BaedalWebApplication.class);
    }
    
    public static void main(String[] args) {
	SpringApplication.run(BaedalWebApplication.class, args);
    }
}

 

in application.yml ..

 

server:
  port: 8888 

spring:
  data:
    maria:
      jdbc-url: jdbc:mysql://{ip주소}:{포트번호}/imsi?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
      driver-class-name: org.mariadb.jdbc.Driver
      username: {mysql 유저}
      password: {mysql 비밀번호}
  jpa:
    properties:
      hibernate:
        #show_sql: true
        format_sql: true
        temp:
          use_jdbc_metadata_defaults: false
    generate-ddl: true

 

 

 

잘 되지 않는다면 아래 링크를 통해 클론 후 동작을 확인할 수 있다.

 

 

https://github.com/jalhagosipo/BaedalWeb/tree/SimpleRestAPI

 

jalhagosipo/BaedalWeb

delivery web-site. Contribute to jalhagosipo/BaedalWeb development by creating an account on GitHub.

github.com

 

 

이제 아래 글을 통해 지금까지 작성한 코드들이 제대로 동작하는지 확인해보자.

https://gaebal-goebal.tistory.com/50