Web/Spring 2019. 3. 22. 15:45

Spring - Springboot GenericController(제네릭컨트롤러), 컨트롤러 추상화



Web applcation을 개발하면 공통적으로 개발하는 Flow가 있다.



Controller->Service->Repository는 거의 왠만한 웹어플리케이션의 개발 플로우일 것이다. 하지만 이런 플로우 안에서도 거의 모든 비즈니스마다 공통적인 로직이있다. 바로 CRUD이다. 모든 도메인에는 생성,수정,삭제,조회 로직이 들어간다. 이러한 로직이 모든 컨트롤러,서비스 클래스에 도메인마다 작성이 된다면 이것도 중복코드인 것이다. 오늘 포스팅할 주제는 바로 이러한 로직을 추상화한 Generic Controller이다. 사실 아직 많이 부족한 탓에 더 좋은 방법도 있겠지만 나름 혼자 고심하여 개발한 것이기 때문에 만약 잘못된 부분이 있다면 지적을 해주셨음 좋겠다.





Entity Class


1
2
3
4
5
6
7
8
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 
@EnableJpaAuditing
@Configuration
public class JpaConfig {
 
}
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.time.LocalDateTime;
 
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
 
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
import lombok.Getter;
import lombok.Setter;
 
/**
 * BaseEntity 추상클래스
 * 해당 추상클래스를 상속할때 @Tablegenerator는 상속하는 클래스에서 정의해야함
 * 또한 id field의 칼럼속성도 필요할때에 재정의해야함
 * @author yun-yeoseong
 *
 */
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Setter
@Getter
public abstract class BaseEntity<extends BaseEntity<?>> implements Comparable<T>{
    
    @Id
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "RNB_SEQ_GENERATOR")
    private long id;
    
    @Column(name="CREATED_DATE",nullable=false,updatable=false)
    @CreatedDate
    private LocalDateTime createdDate;
    
    @Column(name="UPDATED_DATE",nullable=false)
    @LastModifiedDate
    private LocalDateTime modifiedDate;
 
    @Override
    public int compareTo(T o) {
        Long result = getId()-o.getId();
        return result.intValue();
    }
    
}
 
cs


엔티티마다도 공통적인 필드를 가지고 있으므로 BaseEntity 클래스 하나를 정의하였다. 정렬을 위한 compareTo는 자신에 맞게 재정의하면 될 것 같다. @EntityListener 어노테이션을 추가해 AuditingEntityListener 사용을 명시한다. 해당 어노테이션을 붙인 후에 @CreatedDate는 엔티티 생성시점 처음에 생성되는 날짜필드가 되는 것이고, @LastModifiedDate는 엔티티 수정이 될때 매번 수정되는 날짜 필드이다.(@PrePersist,@PreUpdate 등을 사용해도 될듯) 사실 이 두개의 어노테이션말고 @CreatedBy@LastModifiedBy 어노테이션도 존재한다. 이것을 생성자와 수정자를 넣어주는 어노테이션인데, 이것은 스프링 시큐리티의 Principal을 사용한다. 이것은 당장 필요하지 않으므로 구현하지 않았다.(구글링 참조) 그리고 마지막으로는 @Id 필드이다. 해당 생성 전략을 테이블 타입을 이용하였다. 만약 더욱 다양한 타입의 @Id가 필요하다면 아래 링크를 참조하자.


▶︎▶︎▶︎JPA 기본키 매핑 전략!


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.Serializable;
 
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
 
import com.web.rnbsoft.common.jpa.BaseEntity;
 
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
 
/**
 * Intent Entity
 * @author yun-yeoseong
 *
 */
@Entity
@Table(name="RNB_INTENT"
        ,indexes=@Index(columnList="INTENT_NAME",unique=false))
@AttributeOverride(name = "id",column = @Column(name = "INTENT_ID"))
@TableGenerator(name="RNB_SEQ_GENERATOR",table="TB_SEQUENCE",
                pkColumnName="SEQ_NAME",pkColumnValue="RNB_INTENT_SEQ",allocationSize=1)
@Getter
@Setter
@ToString
public class IntentEntity extends BaseEntity<IntentEntity> implements Serializable{
    
    private static final long serialVersionUID = 1864304860822295551L;
    
    @Column(name="INTENT_NAME",nullable=false)
    private String intentName;
    
}
 
cs


BaseEntity를 상속한 엔티티클래스이다. 몇가지 설명을 하자면 우선 BaseEntity에 있는 id 필드를 오버라이드 했다는 것이고, @TableGenerator를 선언하여 BaseEntity의 @GenerateValue에서 참조하여 기본키를 생성할 수 있게 하였다.



Service


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import com.web.rnbsoft.entity.intent.IntentEntity;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
public interface IntentService {
    public IntentEntity findByName(String name);
}
 
/************************************************************/
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.web.rnbsoft.entity.intent.IntentEntity;
import com.web.rnbsoft.repository.intent.IntentRepository;
 
import lombok.extern.slf4j.Slf4j;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
@Slf4j
@Service
public class IntentServiceImpl implements IntentService {
    
    @Autowired
    private IntentRepository intentRepository;
    
    @Override
    public IntentEntity findByName(String name) {
        log.debug("IntentServiceImple.findByName - {}",name);
        return intentRepository.findByIntentName(name);
    }
    
    
}
 
cs


Repository


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import org.springframework.data.jpa.repository.JpaRepository;
 
import com.web.rnbsoft.entity.intent.IntentEntity;
 
/**
 * IntentRepository
 * @author yun-yeoseong
 *
 */
public interface IntentRepository extends JpaRepository<IntentEntity, Long>, IntentRepositoryCustom{
    public IntentEntity findByIntentName(String intentName);
}
 
 
/************************************************************/
 
import java.util.List;
 
import com.web.rnbsoft.entity.intent.IntentEntity;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
public interface IntentRepositoryCustom {
    public List<IntentEntity> selectByCategoryAndName(String category,String name);
}
 
/************************************************************/
 
import java.util.List;
 
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
 
import com.querydsl.jpa.JPQLQuery;
import com.web.rnbsoft.entity.intent.IntentEntity;
import com.web.rnbsoft.entity.intent.QIntentEntity;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
public class IntentRepositoryImpl extends QuerydslRepositorySupport implements IntentRepositoryCustom {
 
    public IntentRepositoryImpl() {
        super(IntentEntity.class);
    }
 
    @Override
    public List<IntentEntity> selectByCategoryAndName(String category, String name) {
        
        QIntentEntity intent = QIntentEntity.intentEntity;
        
        JPQLQuery<IntentEntity> query = from(intent)
                          .where(intent.intentName.contains(name));
                          
        return query.fetch();
    }
 
}
cs


Repository는 QueryDSL을 이용하여 작성하였다. QueryDSL 설정법 등은 아래 링크를 참조하자

▶︎▶︎▶︎Spring JPA+QueryDSL

▶︎▶︎▶︎QueryDSL Query작성법




Controller


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
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 lombok.extern.slf4j.Slf4j;
 
/**
 * 
 * GenericController - 컨트롤러의 공통 기능 추상화(CRUD)
 * 
 * @author yun-yeoseong
 *
 */
@Slf4j
public abstract class GenericController<T ,ID> {
    
    @Autowired
    private JpaRepository<T, ID> repository;
    
    @GetMapping("/{id}")
    public T select(@PathVariable ID id) {
        log.debug("GenericController.select - {}",id);
        return repository.findById(id).get();
    }
    
    @GetMapping
    public List<T> list(){
        log.debug("GenericController.list");
        return repository.findAll();
    }
    
    @PostMapping
    public T create(@RequestBody T t) {
        log.debug("GenericController.create - {}",t.toString());
        T created = repository.save(t);
        return created;
    }
    
    @PutMapping("/{id}")
    public T update(@RequestBody T t) {
        log.debug("GenericController.update - {}",t.toString());
        T updated = repository.save(t);
        return updated;
    }
    
    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable ID id) {
        log.debug("GenericController.delete - {}",id);
        repository.deleteById(id);
        return true;
    }
}
cs


세부로직은 신경쓰지말자. 이것이 공통적으로 컨트롤러가 사용하는 것을 추상화한 GenericController이다. 사실 Service 단까지도 GenericService를 구현하려고 했으나, 크게 복잡한 로직도 아니고 해서 전부 Repository를 주입받아서 사용하였다. 나중에 이 틀을 가지고 다듬어서 사용하면 될듯 싶다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import com.web.rnbsoft.controller.GenericController;
import com.web.rnbsoft.entity.intent.IntentEntity;
import com.web.rnbsoft.service.intent.IntentService;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
@RestController
@RequestMapping("/intent")
public class IntentController extends GenericController<IntentEntity,Long> {
    
    private IntentService service;
    
    public IntentController(IntentService service) {
        this.service = service;
    }
    
    @PostMapping("/search")
    public IntentEntity selectByName(@RequestBody String intentName) {
        log.debug("IntentController.selectByName - {}",intentName);
        return service.findByName(intentName);
    }
}
 
cs


GenericController를 상속받는 Controller이다. CRUD의 공통로직이 모두 제거되었고 물론 이 컨트롤러 말고도 다른 컨트롤러 또한 모두 CRUD 로직은 제거 될것이다. 훨씬 코드가 간결해졌다. 여기서 하나더 기능을 추가하자면 모든 예외처리를 한곳에서 처리해버리는 것이다. 


RestControllerAdvice


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
 
import com.fasterxml.jackson.core.JsonProcessingException;
 
import lombok.extern.slf4j.Slf4j;
 
/**
 * RestControllerAdvice
 * RestController 공통 예외처리
 * @author yun-yeoseong
 *
 */
@Slf4j
@RestControllerAdvice("com.web.rnbsoft")
public class RestCtrlAdvice {
    @ExceptionHandler(value = {Exception.class})
    protected ResponseEntity<String> example(RuntimeException exception,
            Object body,
            WebRequest request) throws JsonProcessingException {
        log.debug("RestCtrlAdvice");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{\"message\":\"example\"}");
    }
}
 
cs

RestController의 모든 공통 예외처리를 해당 클래스에 정의한다. 물론 추후에는 메시지소스를 이용해 응답까지 하나로 통일할 수도 있을 것같다.

여기까지 GenericController를 구현해봤다. 많이 부족한 점이 많은 코드이기에 많은 지적을 해주셨으면 하는 바람이다.

posted by 여성게
: