[Spring] 참고 사항

2020. 8. 24. 23:25Spring/Spring

1. DataSource (MySQL)

url은 '/[데이터베이스명]?[인코딩]&&[서버타임존]&&[SSL 사용 여부]'를 지정한다.

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/SPRINGTEST?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
jdbc.username=root
jdbc.password=

 

 

 

2. JPA Repository

1) 설정

web.xml에 JPA Repository를 사용하기 위한 Spring Data JPA 의존성을 작성한다.

<!-- Spring Data JPA -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>

applicationContext.xml에 Spring Data를 적용하고, Transaction의 경우 JPATransactionManager을 이용하여 어노테이션을 사용할 수 있도록 설정한다. 마지막으로 @Transactional 어노테이션을 설정하기 위한 설정도 적용한다.

<!-- Spring Data -->
<jpa:repositories base-package="com.springAPI.biz.*.impl"/>

<!-- Spring JPA -->
<bean id="jpaVendorAdapter"	class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
<bean id="entityManagerFactory"	class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>
    <property name="packagesToScan">
        <array>
            <value>com.springAPI.biz.user</value>
            <value>com.springAPI.biz.board</value>
        </array>
    </property>
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.format_sql">true</prop>
            <prop key="hibernate.use_sql_comments">true</prop>
            <prop key="hibernate.id.new_generator_mappings">true</prop>
            <prop key="hibernate.hbm2ddl.auto">update</prop>
        </props>
    </property>
</bean>

<!-- Transaction -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!-- @Transactional -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>

2) 비즈니스 컴포넌트, JPA 사용법

제작하는 대부분의 프로젝트 구조로는 Entity(DTO) - Service - ServiceImpl - Repository - Controller로 구성되어있다. Entity는 데이터베이스에 저장하기 위해 유저가 정의한 클래스이며 일반적으로 RDBMS 테이블을 객체화 시킨것으로 생각하면 된다(@Entity, @Id, @Table, @Column).

 

스프링의 Repository의 경우, 인터페이스로 작성하며 Entity의 기본적인 CRUD가 가능하도록 제공한다. Spring Data JPA에서 제공하는 JpaRepository 인터페이스를 상속하기만 해도 되며 인터페이스에 개별적으로 @Repository 어노테이션을 추가할 필요 없다.

 

JpaRepository를 상속받을 때 사용될 Entity 클래스의 ID값이 들어가게 된다(JpaRepository <T, ID>). 단순하게 상속하는 것만으로도 여러 기능을 제공한다. 기본적으로 제공하는 기능 외에도 다른 기능을 추가하고 싶을 때에는 규칙에 맞는 메서드를 추가해야한다.

메서드 설명
save() 레코드를 저장한다. (insert, update)
findOne() PK로 레코드 한건을 찾는다.
findAll() 전체 레코드를 불러온다. (정렬 Sort, 페이징 Pageable 가능)
count() 레코드 갯수를 가져온다.
delete() 레코드를 삭제한다.
findBy로 시작 쿼리를 요청하는 메서드임을 명시한다.
countBy로 시작 쿼리 결과 레코드 수를 요청하는 메서드임을 명시한다.
메서드 키워드 예시 설명
And findByEmailAndUserId(String email, String userId) 여러 필드를 and로 검색한다.
Or findByEmailOrUserId(String email, String userId) 여러 필드를 or로 검색한다.
Between findByCreatedBetween(Date fromDate, Date toDate) 필드의 두 값 사이에 있는 항목을 검색한다.
LessThan findByAgeLessThan(int age) 작은 항목을 검색한다.
GreaterThanEqual findByAgeGreaterThanEqual(int age) 크거나 같은 항목을 검색한다.
Like findByNameLike(String name) like를 통한 항목을 검색한다.
IsNull findByJobIsNull() null인 항목을 검색한다.
In findByJob(String ... jobs) 여러 값중에 하나인 항목을 검색한다.
OrderBy findByEmailOrderByNameAsc(String email) 검색 결과를 정렬하여 전달을 한다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
	
    User findUserByEmailAndUsername(String email, String username);
    Optional<User> findByEmail(String email);
    Boolean existsByEmail(String email);
    Optional<User> findByEmailAndPassword(String email, String password);
}

(JPA 레퍼런스 - https://docs.spring.io/spring-data/jpa/docs/1.10.1.RELEASE/reference/html/#jpa.sample-app.finders.strategies)

3) Pageable

Query 메서드의 입력변수로 Pageable 변수를 추가하면 Page 타입을 반환형으로 사용할 수 있다. Pageable 객체를 통해 페이징과 정렬을 위한 파리미터를 전달한다.

Page<Test> getTest(Pageable pageable) {
    return service.getList(pageable);
}
GET /users?page=1&size=10&sort=createdAt,desc&sort=userId,asc
파라미터명 설명
page 몇번째 페이지 인지를 전달한다.
size 한 페이지에 몇개의 항목을 보여줄 것인지 전달한다.
sort 정렬 정보를 전달하고 이 정보는 필드이름, 정렬방향의 포맷으로 전달된다. 여러 필드의 순차적으로 정렬이 가능하다. (sort = createdAt, desc&sort=userId,asc)

 

 

 

3. Freemarket

web.xml에 Freemarket를 사용하기 위한 의존성을 작성한다.

<!-- Freemarker -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker-gae</artifactId>
    <version>2.3.30</version>
</dependency>
<!-- Freemarker -->
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
    <property name="templateLoaderPath" value="/WEB-INF/views"/>
    <property name="defaultEncoding" value="UTF-8"/>
    <property name="freemarkerSettings">
        <map>
            <entry key="template_update_delay" value="60000"/>
            <entry key="auto_flush" value="false"/>
            <entry key="default_encoding" value="UTF-8"/>
            <entry key="whitespace_stripping" value="true"/>
        </map>
    </property>
</bean>
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
    <property name="order" value="2" />
    <property name="cache" value="true" />
    <property name="suffix" value=".ftl" />
    <property name="contentType" value="text/html; charset=UTF-8" />
    <property name="exposeSpringMacroHelpers" value="true" />
</bean>

 

 

 

4. 연관관계 매핑

1) 연관관계

객체는 참조를 사용하여 연관관계를 맺고 테이블은 외래 키를 사용하여 연관관계를 맺는다.

키워드 설명
방향 회원/팀 관계에서
- 단방향 : 회원 → 팀, 팀 →회원 둘 중 한쪽만 참조하는 관계
- 양방향 : 회원 → 팀, 팀 회원 둘이 서로를 참조하는 관계
다중성 일대일, 일대다 / 다대일, 다대다
연관관계의 주인 양방향 연관관계에서 관계의 주인을 지정

2) 다대일

1. 단방향 연관관계

- 회원은 하나의 팀에만 소속될 수 있다.

- 회원과 팀은 다대일 관계이다. (여러 회원은 하나의 팀에 속한다.)

중심 SQL 자바
회원 중심 SELECT *
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID;
Class Team {
   Long id;
   ...
}

Class Member {
   Long id;
   Team team;
   ...
}

Member a = new Member();
a.getTeam.getId();
팀 중심 SELECT *
FROM TEAM T
INNER JOIN MEMBER M ON M.TEAM_ID = T.TEAM_ID
Class Team {
   Long id;
   Member id;
}

Class Member {
   Long id;
   ...
}

Team a = new Team();
a.getMember.getId();

SQL은 동일한 JOIN으로 양방향 조인이 가능했지만 객체는 각 객체에 참조할 대상을 필드로 넣어주어야 한다.

 

2. JPA 연관관계 매핑

@ManyToOne

- 다대일 관계를 나타내는 매핑 정보 어노테이션이다.

속성 설명
optional (Default true) false로 처리 시 연고나된 엔티티가 반드시 존재해야한다.
fetch 글로벌 패치 전략 설정을 할 수 있다.
cascade 영속성 전이 기능 사용을 할 수 있다.
targetEntity 연관된 엔티티의 타입 정보를 설정한다. 

@JoinColumn

- 외래키 매핑 시 사용하는 어노테이션이다.

- name 속성은 매핑할 외래키의 이름을 지정한다.

- 어노테이션을 생략해도 외래키가 생성이 된다. 생략할 경우 외래키의 이름이 기본 전략을 활용하여 생성된다. (권장하지 않음)

속성 설명
name 매핑할 외래키의 이름을 설정한다.
referencedColumnName 외래키가 참조하는 대상 테이블의 컬럼명을 설정한다.
foreignKey 외래키 제약조건을 지정한다.
unique,nullable,insertable, updateable, columnDefinition, table @Column 속성과 동일하다.
@Entity
@Getter
@Setter
public class Member {
	
    @Id
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
@Entity
@Getter
@Setter
public class Team {
	
    @Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
}

 

3. 단방향 CRUD (회원 엔티티에서 팀 엔티티를 참조할 때)

- 저장

public void save() { 

    Team team = Team.builder()
        .name("team"); 
        .build(); 
        
    em.persist(team); 
    
    Member member1 = Member.builder() 
        .username("member1") 
        .team(team) .build(); 
    
    em.persist(member1); 
    
    Member member2 = Member.builder() 
        .username("member2"); 
        .team(team) .build(); 
    
    em.persist(member2); 
}

- 조회

Member member = em.find(Member.class, member1_id); 
Team team = member.getTeam();
private statid void read(EntityManager em) { 
	
    String jpql = "select m from Member m join m.team t where" + "t.name=:teamName"; 
    
    List<Member> resultList = em.createQuery(jpql, Member.class) 
        .setParameter("teamName", "team1") 
        .getResultList(); 
}

// JPQL 쿼리 변환
SELECT M.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE T.NAME = 'team1';

- 수정

private static void update(EntityManager em) {
	
    Team team2 = Team.builder() 
        .name("team2"); 
        .build(); 
    
    em.persist(team2); 
    
    Member member = em.find(Member.class, member1_id);
    member.setTeam(team2); 
}

- 제거

private static void delete(EntityManager em) { 
	
    Member member1 = em.find(Member.class, member1_id); 
    member1.setTeam(null); 
}

 

4. 양방향 연관관계

mappedBy 속성에는 양방향 매핑 시 반대쪽 매핑(Member 클래스의 Team team) 값을 지정해주어야 한다. 실제 쿼리는 TEAM_ID 하나로 매핑이 가능했지만 객체 참조의 경우 양쪽 다 참조할 필드를 작성해주어야 한다. 이럴 경우에는 두 테이블에 서로의 고유 키를 외래 키로 포함할 필요가 없고 mappedBy 속성을 통해 외래 키의 위치를 정의하여 해결한다.

 

외래키를 가지는 엔티티가 연관관계의 주인이며 mappedBy는 주인을 명시한다.

@Entity
@Getter
@Setter
public class Member {
	
    @Id
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
@Entity
@Getter
@Setter
public class Team {
	
    @Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> memers = new ArrayList<>();
}

 

 

5. 양방향 CRUD

- 저장

주인이 아닌 필드는 변경하더라도 저장되지 않는다는 점을 주의해야한다.

private void updateEntity(EntityManager em) { 

    Team team = em.find(Team.class, team1_id); 
    // 저장되지 않음 
    team.getMembers.add( Member.builder() .name("member3") 
        .build(); 
    );

    // 저장되지 않음 
    team.getMembers.add( Member.builder() 
        .name("member4") .build(); 
    );

    // 저장됨
    Member member1 = em.find(Member.class, member1_id); 
    member1.setTeam(team2);
}

회원의 소속을 변경하고 싶을 경우에는 Member 클래스와 Team 클래스 모두 바꾸어주어야 한다. 테이블에서는 Member 클래스의 team 필드만 변경해주면 되지만 JPA에서는 값이 변하지 않은 순수 자바 객체인 경우 문제가 생길 수 있다.

private void example(EntityManager em) { 
	
    Team team2 = em.find(Team.class, team2_id); 
    
    // member1의 소속 팀 1 
    Member member1 = em.find(Member.class, member1_id); 
    
    // member1의 소속 팀 2로 변경
    member1.setTeam(team2); 
    
    for (Member member : team2.getMembers()) { 
        // member1이 team2의 회원 목록에 없어 출력되지 않는다. 
        System.out.println(member.getUsername()); 
    } 
}

문제는 엔티티를 수정함으로서 간단하게 해결이 가능하다.

@Entity 
@Getter 
@Setter 
public class Member { 
	
    @Id 
    @Column (name = "MEMBER_ID") 
    private Long id; 
    
    private String username; 
    
    @ManyToOne 
    @JoinColumn (name = "TEAM_ID") 
    @Setter (AccessLevel.NONE) 
    private Team team; 
    
    public void setTeam(Team team) { 
    	
        if (this.team != null) { 
            this.team.getMembers().remove(this); 
        } 
        this.team = team;
        team.getMembers().add(this); 
    } 
}

 

 

 

5. 복합키

1) 사용 이유

회원 엔티티에는  'id, name, startDate, endDate, city, street, zipcode, ...' 등 이 존재할 때, 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으므로 명확하게 'id, name, 근무기간, 집 주소'로 묶어주는 것이 좋다. 임베디드 타입은 기본 생성자가 필수이며 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하기 때문에 엔티티와 임베디드 타입의 관계를 컴포지션 관계라고 한다. (하이버네이트는 컴포넌트라고 지칭)

 

임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑할 수 있게 되며 더욱더 객체지향적으로 개발할 수 있게 된다. ORM 애플리케이션은 잘 설계했을 때 매핑 테이블 보다 클래스 수가 더 많다.

2) 사용 방법

Embedded 타입에는 기본 생성자가 필수적으로 존재햐야하며 Serializable 인터페이스를 구현하고 영속성 컨텍스트가 식별자를 비교할 때 사용하기에 equals(), hashCode()를 구현해야한다. @EmbeddedId는 식별 관계로 매핑할 때 @MapsId를 사용하면 된다. @MapsId는 외래 키와 매핑한 연관 관계를 기본 키에도 매핑하겠다는 뜻이며 속성값은 @EmbeddedId를 사용한 필드명을 지정한다.

@Entity
public class Parent {
	
    @Id
    @Column(name = "parent_id")
    private ParentId id1;
    
    private String name;
}
@Entity
public class Child {
	
    @EmbeddedId
    private ChildId id;
    
    @MapsId("parentId") // ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
    
    private String name;
}
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class ChildId implements Serializable {
	
    private String parentId; // @MapsId("parentId) 매핑
    
    @Column(name = "child_id")
    private String id;
    
    @Override
    public boolean equals(Object o) {
    
    }
    
    @Override
    public int hashCode() {
    
    }
    
}
@Entity
public class GrandChild {
	
    @EmbeddedId
    private GrandChildId id;
    
    @MapsId("childId")
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "parent_id", referencedColumnName = "parent_id"),
        @JoinColumn(name = "child_id", referencedColumnName = "child_id")
    })
    private Child child;
    
    private String name;
}
@Embeddable
public class GrandChild implements Serializabe {
	
    private ChildId childId // @MapsId(childId) 매핑
    
    @Column(name = "grandchild_id")
    private String id;
    
    @Override
    public booleans equals(Object o) {
    
    }
    
    @Override
    public int hashCode() {
    
    }
}

 

 

 

6. Optional

1) 사용 이유

간단히 설명한다면 메서드 반환값이 없다는 걸 표현하기 위해 사용한다. 메서드가 반화될 결과값이 없음을 명백하게 표현할 필요가 있고, null을 반환하면 에러가 유발할 가능성이 높은 상황에서 메서드의 반환 타입으로 Optional을 사용하자는 것이 Optional을 만든 주된 목적이다. Optional 타입의 변수 값은 절대 null이어서는 안되며 항상 Optional 인스턴스를 가리켜야한다.

2) orElse(), orElseGet(), orElseThrow()

orElse()는 Optional에 값이 존재하던 존재하지 않던 무조건 실행이 된다. 따라서 새로운 객체를 생성하거나 새로운 연산을 수행하는 경우에는 orElse() 보다 orElseGet()을 사용해야한다.

Optional<Test> test = ...;
return test.OrElse(null);

Optional<Test> test = ...;
return test.orElseThrow() -> new NoSuchElementException();

Optional<Test> test = ...;
return test.orElseGet(Test::new);

Test EMPTY_TEST = new Test();
...
Optional<Test> test = ...;
return test.orElse(EMPTY_TEST);

 

 

 

7. 참고 어노테이션

1) @NoArgsConstructor

기본 생성자를 자동으로 생성해준다. (No Argument)

2) @AllArgsConstructor

모든 필드값을 포함한 생성자 자동으로 생성해준다. (All Argument)

3) @RequiredArgsConstructor

초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자를 생성해준다. @Autowired 어노테이션 대신 사용할 때 사용하기도 한다.

@RequiredArgsConstructor
public class ServiceImpl implements Service {

    private final Repository repository;
    
}

4) @Getter, @Setter

Getter/Setter를 자동으로 생성해준다.

5) @Entity, @Id, @GeneratedValue

엔티티 클래스임을 명시해주고 PK 값에 해당하는 값에 @Id 어노테이션을 설정해준다. @GeneratedValue의 strategy 속성은 하이버네이트의 자동 키 생성 전략을 결정하도록 해준다.

전략 설명
AUTO(Default) JPA 구현체가 자동으로 생성 전략을 결정한다.
IDENTITY 기본키 생성을 데이터베이스에 위임한다. 대표적으로는 MySQL의 AUTO_INCREMENT를 생각할 수 있다.
SEQUENCE 데이터베이스의 특별한 오브젝트 시퀀스를 사용하여 기본키를 생성한다.
TABLE 데이터베이스에 키 생성 전용 테이블을 하나 생성하고 이를 이용하여 기본키를 생성한다.

5) @CookieValue

쿠키값을 받아낼 수 있는 어노테이션이다.

@CookieValue(value = "accessToken")

Postman 쿠키 설정

6) @Modify, @Query

JPA는 EntityManager가 자동으로 변경을 감지하여 반영을 하기에, 변경된 내용만을 수정하면 되지만, 복잡한 쿼리문일 경우 @Modify, @Query 어노테이션을 이용하여 데이터베이스에 반영할 수 있다. (일반적으로 @Transactional 어노테이션은 클래스 전체에 씌우는 경우가 대부분이지만, 이처럼 'readOnly' 속성을 이용하지 않을 경우 모든 메서드마다 각각 적용해주어야 한다.)

@Modifying
@Query("UPDATE Test t SET t.test = :test WHERE t.testId = :testId")
@Transactional
void modifyTest(@Param("test") String token, @Param("testId") int userId);

[참고] http://blog.breakingthat.com/2018/03/16/jpa-entity-%EB%B3%B5%ED%95%A9pk-%EB%A7%B5%ED%95%91-embeddedid-idclass/

[참고] https://woowabros.github.io/experience/2019/01/04/composit-key-jpa.html

[참고] https://ict-nroo.tistory.com/117

[참고] https://jogeum.net/9

[참고] http://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/

[참고] https://lng1982.tistory.com/286

[참고] https://private-space.tistory.com/95?category=876554

[참고] https://jobc.tistory.com/120

728x90