Java/Spring 조각 기록(3)

ivor·2021년 11월 30일
0

spring

목록 보기
3/3

DB, Java

application.properties 파일에 DB 관련 설정 중 모르는 것이 나와 찾아보았다.

// application.properties
spring:
  jpa:
    generate-ddl : true
    hibernate : 
      ddl-auto : update
    show-sql : true

JPA ?

jpa란 Java Persistence API의 준말로 현재 자바 진영의 ORM 기술 표준으로 '인터페이스 모음'이라고 한다.

ORM ?

ORM은 Object-Relational Mapping의 준말. 객체는 객체대로, 관계형 DB는 관계형 DB대로 설계하면 ORM이 중간에서 매핑해주는 것.

정리해보면 Java로 관계형 데이터베이스(RDB)에 접근할 수 있게 해주는 중간자 역할인 것 같다.
참조한 문서를 조금 더 읽어보니 JPA는 내부에서 JDBC를 이용하여 DB와 통신한다고 한다.

Hibernate ?

앞서 언급했듯 JPA는 인터페이스 모음이기에 구현체가 필요하다. hibernate는 그러한 구현체 중 하나라고 한다.
즉 위의 코드는 jpa를, 그 중 hibernate를 구현체로 삼아서 사용하겠다는 의미겠지.

(ddl은 뭘까? Data Definition Language로 '데이터 정의어'라고 한다. CREATE, ALTER, DROP, RENAME 등의 명령어가 이에 해당한다고 한다.)

spring.jpa.generate-ddl : true@Entity가 명시된 class를 찾아 ddl을 생성하고 실행한다고 한다.
spring.jpa.hibernate.ddl-auto: update는 Entity class와 DB 구조를 비교하여 DB 구조에 없는 것을 자동으로 추가한다고 한다.

즉 위의 일련의 과정들은 Java에서의 변경이 DB에서도 이루어질 수 있도록 하는 설정이라는 것


@Entity

@Entity의 적용

다음의 코드를 살펴보자.

@Entity
public class Member{

}

여기까지만 코드를 쓰면 오류가 발생한다 → PK(Primary Key)가 없다는 것.
@Entity가 붙은 클래스를 RDB와 연결한다고 했다. RDB 테이블에는 PK가 반드시 필요하다.
코드를 다음처럼 수정하자.

@Data
@Entity
public class Member{

  @Id
  private String userId;
  
  private String userPw;
  private String userName;
}

@Data로 getter, setter 등의 설정을 skip하고(알아서 해준다), @Entity로 JPA가 DB와 연동할 클래스라는 것을 알게 해준다. (@Id를 통해 PK를 설정하는 것 같다.)

Entity와 Repository

Entity와 Respository는 보통 세트를 이룬다.

위에서 만든 Member 클래스를 entity라는 패키지에 넣고, 해당 패키지와 병렬적으로(?) repository 패키지를 만든다.
그 후, MemberRepository라는 인터페이스를 그 패키지 안에 만든다.

package ~.fastlms.member;

public interface MemberRepository extends JpaRepository<Member, String>{ // 'Member'는 table명, 'String'은 key

}

실행시키고 Database 탭을 새로고침하면 tables 밑에 member라는 테이블에 생성되었고, 해당 table 밑 columns 폴더에 우리가 설정한 parameter에 대한 data 종류들(user_id, user_pw, user_name)이 칼럼으로 생성된 것을 확인할 수 있다.
(table은 만들어졌는데 data가 없는 이유는 무엇일까? → 아직 DB에 저장을 하지 않았기 때문이다. 우리는 지금까지 JPA 설정을 하고, Member클래스에 @Entity를 붙이고, 세트로 사용할 MemberRepository 인터페이스를 생성했을 뿐이다.)

🤨 JpaRepository는 또 뭐야?

spring에서 지원하는 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다. 구현 클래스를 만들지 않고 인터페이스만으로도 DB에 접근할 수 있도록 한다.
JpaRepository를 상속 받으면 여러 메소드들을 사용할 수 있는데 이 메소드 이름만으로 쿼리를 생성할 수 있다.

예를 들어

public interface MemberRepository extends JpaRepository<Member, String>{
  Optional<Member> findByAge(String age);
}

위의 코드는 Member 클래스로 인해 만들어진 테이블에서 age를 key로 data를 추출할 수 있다.

Optional에 대해서는 다음을 참고
https://mangkyu.tistory.com/70


<form>으로 받은 정보를 DB에 저장

Java/Spring 조각 기록(2)과 위의 내용을 참고하여 입력받은 정보들을 DB에 저장하는 방법을 알아보자.

@Data
public class MemberInputs{
  private String ID;
  private String PW;
  private String name;
}

우선 입력받을 값들에 대해 MemberInputs에 멤버변수를 만들어 놓고
(또 @Data를 통해 getter, setter를 선언하지 않고도 사용할 수 있도록 한다.)

@Data
@Entity
public class Member{
  @Id
  private String userId;
  
  private String userPw;
  private String userName;
}

Member 클래스를 생성하고 @Entity를 통해 JPA에게 이 클래스를 DB와 연동할거라고 알려주고

@PostMapping("...")
public String memberRegisterSubmit(MemberInputs infos){
  Member member = new Member();
  member.setUserId(infos.getID());
  member.setUserPw(infos.getPW());
  member.setUserName(infos.getName());
  
  memberRepository.save(member);
  
  return "...";
}

MemberController 내부의 메소드에서 "..." url에서의 post요청이 발생하였을 때 MemberInputs 클래스형의 infos(여기엔 ID, PW, name parameter에 사용자가 입력하고 post 요청을 보낸 값들이 들어있다)를 이용하여 각 parameter에 대한 값을 Member 인스턴스 member 멤버 변수에 할당한다.

그 후, memberRepository에 member를 save()하는데, memberRepository가 JpaRepository를 상속받았기 때문에 별다른 구현체 없이도(memberRepository 내에 save라는 메소드를 구현하지 않았다) save 메소드를 사용하여 쿼리를 작성하고, DB member 테이블에 data를 저장할 수 있다.

❗️ 위 방법의 문제점

userId의 값을 abc@naver.com으로 입력한 사람이 있다고 가정하자. 해당 data는 DB에 저장되었을 것이다.(user_id column에 abc@naver.com이 저장된 채로)
그 후 또다른 사람이 userId의 값을 abc@naver.com으로 입력했다고 생각해보자. 그러면 해당 data가 DB에 저장되는데 이 때 user_id=abc@naver.com였던 이전 사람의 data는 새로운 사람의 data로 덮어써진다. user_id는 PK라 중복이 허용되지 않는다. 하지만 잘못된 입력으로 애꿎은 사람의 data만 덮어씌워진 것이다.

🧐 이를 방지하려면 어떻게 해야할까?

fastlms/member 아래에 service라는 디렉토리를 만들고 해당 디렉토리 내에 MemberService라는 인터페이스를 만들자.

package ~.fastlms.member.service //'~'는 실제로 쓰는 것이 아닌 생략의 의미

import ~.fastlms.member.model.MemberInputs;

public interface MemberService{
  boolean register(MemberInputs infos);
}

그리고 해당 인터페이스의 구현체를 만들어야 한다. 앞으로 구현체들을 저장하기 위해 service 디렉토리 아래에 impl 패키지를 만들자. 그리고 해당 패키지 내에 MemberServiceImpl 클래스를 만든다.

package ~.fastlms.member.service.impl

import ~.fastlms.member.model.MemberInputs;
import org.springframework.stereotype.Service;
//(후략)

@RequiredArgsConstructor // 아래의 설명을 읽어볼 것. 간략히 말해 지금 이 코드는 아래에서 선언한 memberRepository에 대한 생성자를 별도로 명시하여 만들지 않아도 되게 한다.
@Service
public class MemberServiceImpl implements MemberService{
  private final MemberRepository memberRepository;
  
  @Override
  public boolean register(MemberInputs infos){
    
    Optional<Member> optionalMember = memberRespository.findById(infos.getUserId())
    
    if(optionalMember.isPresent()){ // 지금 입력받은 datas에 대해 userId값을 기준으로 DB 내 datas를 조회해봤을 때 존재한다면
      return false; // 바로 false를 return하고 메소드 종료
    } // 즉 이미 존재하는 userId(여기서는 email)를 입력할 경우 덮어써지지 않게끔.
    
    Member member = new Member();
    member.setUserId(infos.getID());
    member.setUserPw(infos.getPW());
    member.setUserName(infos.getName());
  
    memberRepository.save(member);
    return true; //회원 정보 DB 저장 여부(true) 
  }
}

그 이후 MemberController 코드를 다음처럼 수정하자.

@Controller
public class MemberController{
  private final MemberService memberService;
  
  public MemberController(MemberService memberService){
    this.MemberService = memberService;
  } //생성자
}

(이때 보다 간편한 방법이 있다. lombok의 @RequiredArgsConstructor를 이용하는 것)

@RequiredArgsConstructor// 생성자를 만들지 않아도 된다. 
@Controller
public class MemberController{
  private final MemberService memberService;
  
  @PostMapping("...")
  public String memberRegisterSubmit(MemberInputs infos){
    boolean result = memberService.register(infos); //result에는 회원 정보 DB 저장 여부가 담긴다. false라면 동일한 user_id가 이미 존재하기 때문에 저장이 되지 않았다는 뜻. true면 동일한 user_id가 없고 때문에 입력받은 data들이 Member 인스턴스로 담겨 MemberRepository의 save메소드를 이용해 DB에 저장되었다는 뜻이다.
    
    return "...."; // 회원 정보 저장 완료와 미완료 시 표기되는 html을 다르게 해야할 것.
} 

여기까지 왔다면 패키지들은 다음과 같을 것이다.

  • controller
    • MemberController
  • entity
    • Member
  • model
    • MemberInputs
  • repository
    • MemberRepository
  • service
    • MemberService
    • Impl
      • MemberServiceImpl

Summary

📌 JPA, @Entity, class

  • ORM 기술 중 하나인 JPA를 이용(Hibernate로 구현)하여 Java 언어로 DB에 접근할 수 있다.
    • DB에 table로 만들고 싶은 클래스에 @Entity를 붙인다.
    • @Id로 PK를 설정한다.

📌 interface Repository, JpaRepository

  • JpaRepository를 상속받음으로써 메소드 구현 없이 선언만으로 DB에 쿼리를 쏠 수 있다.
    • Ex.
      • Optional<Member> findById(String id);
      • Optional<Member> findByAge(Integer age);

📌 Service, 중복 방지

  • 중복된 ID(email)을 입력받을 경우 기존 data가 새로운 data로 덮어씌워질 위험성이 있다.
    • MemberSerive interface, MemberServiceImpl class를 만들고 메소드를 이용하여 true or false를 return한다.
      • true에 대해서만 DB에 저장이 되게 한다.
    • 해당 return 값을 이용하여 보여줄 페이지를 다르게 한다.
      • true 시 인증 단계로, false 시 회원 가입 불가 메세지를 띄우는 식으로.

Questions

🤔 Member, MemberInputs, MemberRepository 등으로 나누어서 (중복되는 것처럼 보이는) 작업들을 굳이 수행해야 하는 이유는 뭘까?

  • 지금까지 배운대로 생각해봤을 때...
    • Member 클래스는 DB에 table을 만드는 데에 쓰인다. @Entity를 붙이고 JPA를 이용하여 DB에 table명이 'Member'이고 column명이 'userId', 'userPw', 'name'인 column 구조를 만든다.
    • MemberInputs는 @Data를 이용하여 보다 쉽게 입력받은 값에 접근하기 위해 사용한다.
    • MemberRepository는 interface이지만 JpaRepository를 상속받음으로써 구현체 없이 메소드를 선언하기만 해도 자동으로 DB에 대한 쿼리를 쏠 수 있다.
  • 순서는 다음과 같다.
    Member DB에 table을 생성해둔다.(table명은 'Member', column들은 'user_id', 'user_pw', 'name')
    register.html ID, PW, name 등을 입력 받는다
    MemberInputs POST 요청 시 클래스 내 멤버변수와 일치하는 parameter들을 가지고 있는다
    → Entity인 Member에 각각의 값들이 저장된다
    MemberRepository 상속받은 JpaRepository 때문에 save 메소드를 사용할 수 있고 이를 이용해 Member 인스턴스의 각 값들을 DB에 쏴줄 수 있다
  • Member, MemberRepository는 필요하다고 생각한다.
    @Entity를 붙여 table 구조를 만들 class가 필요하고,
    JpaRepository를 상속받을 interface가 필요하다.
    MemberInputs는 없어도 대체할 방법이 있다.

🤔 MemberService class를 선언하지 않고 MemberService interface, MemberServiceImpl class 두 단계를 거치는 이유가 뭘까?

  • MemberRepository의 경우에는 JpaRepository의 사용법이 그러하다고 생각했다.(인터페이스와 구현되지 않고 선언만 된 메소드만으로 쿼리문을 만들어낼 수 있도록.)
  • 단순히 ID 중복체크를 하는 로직은 MemberService(class)로도 충분히 구현할 수 있는 것 같은데 굳이 interface를 쓴 이유는?
    • Java 'interface' 챕터를 더 공부해봐야겠다.

🤔 MVC 패턴 등 웹 사이트를 만드는 패턴이 있다고 들었다.

  • 구체적으로 어떤 건지 아직 감이 잘 오지 않는다. 대략적으로 어떤 것이 view이고 controller인지는 알겠는데 model이 정확히 어떤 의미인지 와닿지가 않는다.

참조

profile
BEST? BETTER!

0개의 댓글