[Spring Boot] MySQL 연동

늦잠·2024년 4월 26일
1

저번 포스팅에선 간단한 스프링부트 프로젝트를 만들어 동작을 확인했다 -
https://velog.io/@woolzam/Spring-Boot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0

이번엔 프로젝트에 MySQL 데이터베이스를 연결하면서, 갑자기 궁금해진 부분도 같이 적어보겠다.

(MySQL 설치에 대해선 다루지 않았다.)



dependency 추가하기

MySQL과 프로젝트를 연결하기 위해 JPAMySQL Connector/J, 두 개의 dependency를 적용한다.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'

build.gradle의 dependencies에 다음을 써넣어 dependency를 추가한다.

gradle이 아니라 maven이나 kotlyn을 사용했다면 mvnrepository.com에서 검색. 위 코드처럼 버전을 명시하지 않았을 경우 최신 버전이 적용된다.

  • JPA : 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스 모음. 쉽게 말하면 자바 - DB를 연결하는데 필요한 틀.

  • MySQL Connector/J : 자바와 MySQL을 연결하는데 필요한 드라이버.

runtimeOnly vs compileOnly vs implementation :
위에 dependency를 추가할 때 위는 implementation으로, 아래는 runtimeOnly로 써넣었다. 차이는 무엇일까?

  • runtimeOnly :
    라이브러리가 런타임 때만 필요할 경우. 해당 라이브러리에서 변경이 일어나도 컴파일 다시 할 필요 없다.
    ex) db 드라이버는 db와 연결될 때, 로그는 프로그램이 실행되며 로그 찍을 때만 필요하므로 런타임 때만 사용.
  • compileOnly :
    라이브러리가 컴파일 때만 필요한 경우. 빌드 결과물엔 해당 라이브러리가 포함되지 않는다.
    ex) lombok을 통해 @Getter, @Setter 추가하는 건 컴파일 때만 필요.(gradle버전 올라가면서 compileOnly대신 다른걸 사용한다함)
  • implementation :
    라이브러리가 컴파일과 런타임, 양쪽에 모두 필요할 경우.


프로젝트 설정

MySQL과 연결하여 어떻게 DB를 다룰 지 application.yml(yaml), 혹은 application.properties 파일에서 설정한다. 이 포스팅에선 yml을 사용한다.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/(db이름)?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: (사용자이름)
    password: (비밀번호)

  jpa:
    database-platform: org.hibernate.dialect.MySQL8Dialect
    open-in-view: false
    show-sql: true
    hibernate:
      format_sql: true
      ddl-auto: create

사용한 설정은 위와 같다.

이제까지 프로젝트를 만들 땐 설정 내용은 대략적으로만 이해하고 복사-붙여넣기를 해와서 이번에 자세히 보려고 한다.

Datasource 설정

Datasource는 DB와의 Connection을 획득하는 데 사용되는 객체.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/(db이름)?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: (사용자이름)
    password: (비밀번호)
  • url : 연결된 db의 url. 현재는 로컬의 3306포트로만 db를 연결했기 때문에 주소 부분에는 localhost:3306이 있고, 이후 db 스키마 이름을 넣습니다.
  • driver-class-name : 사용할 JDBC 드라이버 클래스. 기입하지 않으면 스프링부트 프레임워크가 추론을 통해 채워 넣는다.
  • username, password : db 접속에 사용될 사용자명과 비밀번호.

Connection Pool

Connection Pool은 말 그대로 여러 db connection을 저장해 두고 있는 곳이다. db와의 connection생성하는 것은 비용이 매우 많이 든다. 때문에 매번 db와 연결할 때 마다 새로운 connection을 생성하고 소멸시키기 보단, Connction Pool에서 예전에 생성한 connection가져와 사용하고 다시 반납하면 비용을 절감할 수 있다.

HikariCP, Tomcat pooling Datasource, Commons DBCP2, H2 JdbcDataSource 등 다양한 Connection Pool 종류가 있지만 Spring Boot 2.0 이후로는 HikariCP가 디폴트로 적용된다고 한다. dependency 또한 spring-boot-starter-data-jpa에 포함되어 있기 때문에 dependency를 추가할 필요는 없다.

jpa 설정

jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    open-in-view: false
    show-sql: true
    hibernate:
      format_sql: true
      ddl-auto: create
  • database-platform : 사용할 db 플랫폼을 명시한다.
  • open-in-view : false일 경우 트랜잭션이 끝난 후 영속성 컨텍스트가 닫혀 바로 connection을 돌려주지만 true일 경우 트랜잭션 범위를 벗어난 시점까지도 영속성 컨텍스트를 열어둘 수 있다. 디폴트값인 true로 둘 경우 트랜잭션이 끝나도 연결이 살아있어 지연로딩 등을 사용할 수 있게 되지만 connection 반환이 늦어 서버가 다운될 수도 있다고 한다. (나중에 지연로딩같은 부분을 처리하게 된다면 다시 공부)
  • show-sql : true로 두면 실행되는 sql 쿼리를 로그로 보여준다.
  • format_sql : true일 경우 쿼리문을 좀 더 예쁘게 보여준다. show-sql 옵션이 꺼져있으면 필요없는 옵션이다.
  • ddl-auto :
    다음과 같은 옵션이 올 수 있다.
    • create :
      entity로 등록된 클래스를 db의 테이블생성한다. 같은 테이블이 이미 있을 경우 해당 테이블을 먼저 삭제한다.
    • create-drop :
      create와 같게 동작하되, 애플리케이션이 종료될 때 생성한 테이블을 삭제된다는 부분이 추가된다.
    • update :
      entity로 등록된 클래스를 db의 테이블로 생성한다. 이미 있다면 해당 테이블을 등록 클래스와 비교하여 다른 부분을 수정한다.
    • validate :
      entity로 등록된 클래스가 db의 테이블로 존재하는 지 확인한다. 없거나 컬럼이 다르다면 애플리케이션을 종료시킨다.
    • none :
      아무 일도 일어나지 않는 디폴트 설정이다.

ddl-auto 옵션같은 경우, 실제 서비스를 운영할 땐 딱 봐도 위험하게 생긴 create, create-drop 옵션은 물론 컬럼을 변형시키는 update도 사용하지 말아야한다.



코드

Entity

@Entity(name="simpleentity")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SimpleEntity {

    public SimpleEntity(String name){
        super();
        this.name = name;
    }

    @Id @GeneratedValue
    private long id;
    private String name;
    
}

간단하게 만든 Entity 클래스이다. 이런 Entity 클래스는 db의 테이블과 매핑되게 된다.
@Entity 어노테이션을 기입해 이 클래스가 Entity임을 프레임워크에 알려준다. name 옵션으로 db의 테이블 이름을 지정할 수도 있고 옵션을 넣지 않으면 자동으로 클래스 이름과 같은 이름의 테이블과 매핑하게 된다.
그 아래 @Data 등 다른 어노테이션은 편의를 위해 기입한 것이다.

@Id를 기입한 id가 기본키가 된다. @GeneratedValue를 기입하면 자동으로 생성되어 1씩 늘어난다.

id가 자동으로 생성되기 때문에 실제 사용되는 값은 name이다.

Controller

@RestController
@RequestMapping("api/v1/simple")
public class SimpleController {

    @Autowired
    SimpleService simpleService;

    @GetMapping("/db/search/{name}")
    public ResponseEntity<List<String>> searchName(@PathVariable String name){
        List<String> names = simpleService.searchName(name);

        if(names == null){
            return ResponseEntity.status(400).body(null);
        }
        return ResponseEntity.status(200).body(names);
    }

    @PostMapping("/db/insert")
    public ResponseEntity<SimpleEntity> insertData(@RequestBody String name){
        SimpleEntity ret = simpleService.insertSimpleEntity(name);

        if(ret == null){
            return ResponseEntity.status(400).body(null);
        }
        return ResponseEntity.status(200).body(ret);
    }

}

데이터를 db에 넣는 insertData 함수와 데이터를 검색하는 searchName 함수를 간단하게 Controller에 구현했다. (필요한 값이 문자열 하나이기에 따로 Request나 Response 클래스를 구현하진 않겠다.)

Service

@Service
public class SimpleService {

    @Autowired
    SimpleEntityRepository simpleEntityRepository;

    public SimpleEntity insertSimpleEntity(String name){
        return simpleEntityRepository.save(new SimpleEntity(name));
    }

    public List<String> searchName(String name) {
        List<SimpleEntity> list = simpleEntityRepository.findByName(name);
        List<String> ret = new ArrayList<>();

        for(SimpleEntity se : list){
            ret.add(se.getName());
        }
        return ret;
    }

}

로직이 처리될 간단한 Service 클래스이다. insertSimpleEntity함수는 데이터를 db에 넣게 되고, searchName함수는 db에서 이름을 검색하여 맞는 데이터를 리턴한다. 이 두 함수는 simpleEntityRepository라는 Repository를 통해 쿼리를 실행하게 된다.

Repository

@Repository
public interface SimpleEntityRepository extends JpaRepository<SimpleEntity, Long> {
    
    public List<SimpleEntity> findByName(String name);
}

간단하게 구현한 Repository 인터페이스이다.
@Repository 어노테이션을 기입해 이 인터페이스가 Repository임을 프레임워크에 알려준다.

extends JpaRepository<SimpleEntity, Long> 

부분을 통해 JpaRepository 인터페이스를 상속한다. 제네릭 안에는 두 값이 들어가는데, 첫 번째는 Entity 클래스, 두 번째는 기본키의 클래스이다.

Repository메서드는 그 이름으로 동작하게 된다.
위의 findByName(String name) 메서드는 이름에 맞춰 name 칼럼을 참조하여 db에서 검색하게 된다.

간단한 방법이지만 표기에 유의해야하며 복잡한 쿼리를 실행하기도 힘들기 때문에 따로 JPQL이라는 특정 데이터베이스에 의존하지 않는 쿼리 언어를 제공하기도 한다.

JPA로 쿼리를 진행하는 방법은 이외에도 몇 가지가 더 있으며 이런 문법에 대해서는 이 글에선 다루지 않겠다.

결과

swagger를 통해 테스트를 해보았다.

name이 name1이라는 데이터를 API를 통해 보내고

연결되어 있는 MySQL db를 확인하면 데이터가 잘 들어가 있는 것을 확인할 수 있다.


참고

profile
피카

0개의 댓글