기본 콘텐츠로 건너뛰기

9월, 2018의 게시물 표시

냉장고 가계부 프로젝트 31

fridge-web에서 fridge 개인화 영역을 아직 구현하지 못했다. 개인화 데이터의 관리는 별도의 도메인으로 관리하기 위해서 fridge-member 프로젝트를 생성한다. fridge-member 의존성 라이브러리는 다음과 같다. <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-java8</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Spring Cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-cli

냉장고 가계부 프로젝트 30

Travis CI를 사용하려고 준비를 다하고 github에 커밋했는데 maven build failed 에러가 났다. 확인해보니 클래스 생성자가 이미 정의되어 있다는 에러였다. [ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] /Users/gang-yeongho/Documents/git_repo/fridge/fridge/src/main/java/com/poseidon/food/model/FoodRequest.java:[11,1] constructor FoodRequest() is already defined in class com.poseidon.food.model.FoodRequest [ERROR] /Users/gang-yeongho/Documents/git_repo/fridge/fridge/src/main/java/com/poseidon/fridge/model/Fridge.java:[26,1] constructor Fridge() is already defined in class com.poseidon.fridge.model.Fridge [ERROR] /Users/gang-yeongho/Documents/git_repo/fridge/fridge/src/main/java/com/poseidon/food/model/Food.java:[25,1] constructor Food() is already defined in class com.poseidon.food.model.Food [ERROR] /Users/gang-yeongho/Documents/git_repo/fridge/fridge/src/main/java/com/poseidon/fridge/model/FridgeRequest.java:[11,1] constructor FridgeRequest() is already defined in class com.poseidon

냉장고 가계부 프로젝트 29

Ribbon을 통해서 API서버와의 로드밸런싱은 잘 작동한다. 하지만, fridge-service 서버가 추가로 등록되면 application.yml 파일을 열고 listOfServers 프로퍼티에 추가로 등록된 서버의 주소를 입력하고 다시 빌드해야하는 단점이 있다. 아이피, 포트를 직접 환경설정파일에 할당하는것도 마찬가지다. Eureka 서버를 구성하고, fridge-service, fridge-web 서비스가 Eureka Client가 되어 Eureka 서버에서 서버목록을 제공받을 수 있으면, 위의 문제점을 해결할 수 있다. fridge-eureka 프로젝트를 생성한다. 의존성은 Cloud Discovery 의 Eureka Server를 체크하고 Finish를 클릭한다. SpringBootApplication 파일명을 EurekaServiceApplication으로 변경하고, @EnableEurekaServer 애너테이션을 추가한다. @EnableEurekaServer @SpringBootApplication public class EurekaServiceApplication { public static void main(String[] args) { SpringApplication.run(EurekaServiceApplication.class, args); } } application.properties 파일을 yml로 변경하고 다음과 같이 설정을 편집한다. spring: application: name: eureka-server server: port: 8761 eureka: client: register-with-eureka: false fetch-registry: false logging: level: com.netflix.eureka: off com.netflix.discovery: off 애플리케이션명은 eureka-server 이며, port 8761번을 사용한

냉장고 가계부 프로젝트 28

Spring Boot 버전을 2.0.5.RELEASE로 변경한다. 수정되는 부분은 많지 않은데 Spring Data JPA의 CrudRepository 에서 제공하는 findById 메서드가 생겼으므로, 기존에 작성한 repository의 findById 메서드는 삭제한다. fridge-web 에서는 thymeleaf dialect 가 제공되는 부분이 이제는 명시적으로 의존성을 추가해야 layout을 사용할 수 있다. pom.xml에 다음과 같이 추가한다. <dependency> <groupId>nz.net.ultraq.thymeleaf</groupId> <artifactId>thymeleaf-layout-dialect</artifactId> </dependency> Application.class 파일에서 LayoutDialect 빈을 설정해야 한다. @Bean public LayoutDialect layoutDialect() { return new LayoutDialect(); } 그리고 contents html 파일들의 decorate 파일 위치를 다음과 같이 선언해야한다. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{templates/default}"> 그 외의 자잘한 수정만 하면 마이그레이션은 끝난다. 테스트를 실행해서 문제가 없는지 확인한다. fridge 프로젝트의 application.properties 파일을 yaml로 변경한다. YAML은 좀더 가독성이 좋다. YAML에 대

냉장고 가계부 프로젝트 27

스프링 Data 에서 제공하는 @CreatedDate, @LastModifedDate 애너테이션을 이용해서 작성일자, 수정일자를 도메인객체가 생성되고, 수정될 때 관리될 수 있도록 한다. Spring Data를 쓰기 전에는 DB컬럼에 Date 타입의 등록일 컬럼을 추가하고 DEFAULT 값으로 SYSDATE 를 줘서 DB에 Insert 되는 시간에 등록일자 컬럼이 채워지는 방식을 사용했었고, Update 쿼리가 실행될때 수정일자 컬럼에 SYSDATE를 직접 Update 해주는 방식을 자주 썼다. 그 뿐만아니라, 등록일자와 더불어 등록한 사용자(ex: registerUser)를 식별하는 식별키(ex: userId, username ...) 를 Insert 쿼리가 실행될때 등록해주고, 수정한 사용자(ex: modifedUser)를 Update 쿼리에 설정하는 방식은 차후에 있을지도 모를 일에 대비해서 늘 반복해서 작업했다. 스프링 Data 에서 제공하는 JPA Audit 기능은 이런 코드의 반복을 줄여준다. 사용자 같은 경우 객체로 넘기면 객체의 식별자가 담긴다. 등록시간은 날짜형 타입이다. public class Blog { @CreatedBy private User user; @CreatedDate private LocalDateTime creadtedDate; } Blog 클래스의 User 객체는 @CreatedBy 애너테이션으로 선언되서 등록한 사용자를 나타내는 컬럼에 값을 입력할것이다. createdDate 필드는 도메인객체가 영속성 저장소에 반영되는 시간을 나타낸다. Fridge, Food 클래스에 등록일, 수정일만 먼저 적용한다. 두 클래스에 createdDate, lastModifedDate 멤버변수를 선언한다. @Data @NoArgsConstructor @Entity @EntityListeners(AuditingEntityListener.class) public class Fridge {

냉장고 가계부 프로젝트 26

Fridge 프로젝트 소스코드를 정리한다. 먼저, FridgeRepository에서 findByUserId 메서드를 기존의 Fridge 객체를 리턴타입으로 가지는데 Optional<Fridge> 리턴값으로 변경한다. 그리고 기존에 사용하던 단일 로우를 리턴하는 findOne(long id) 메서드(JpaRepository 에서 제공)를 사용하지 않고 findById라는 메서드를 추가한다. 이 메서드의 리턴값도 Optional<Fridge> 리턴타입으로 한다. @Repository public interface FridgeRepository extends JpaRepository<Fridge, Integer> { Optional<Fridge> findByUserId(Long userId); Optional<Fridge> findById(Integer id); } Java8 에서 나온 Optional은 널포인트오류를 처리하기 위한 java.util 유틸리티 클래스이다. 기존코드는 Null이 나올경우 if(data == null) 이런식으로 처리하거나, Null객체를 만들어서 사용했었는데, 이제는 Optional 을 이용해서 Null일경우 어떻게 처리할지를 일관성있게 명시할 수 있다. FridgeRepository의 findByUserId, findById 두 메서드를 사용하는 곳은 FridgeController 이므로 FridgeController 클래스를 수정한다. public class FridgeController { ... @GetMapping("/{id}") Resource<Fridge> loadFridgeById(@PathVariable final int id) { Fridge fridge = repository.findById(id) .orElseThrow(() -> new

냉장고 가계부 프로젝트 25

유통기한이 임박한 식품을 강조하기 위해서 UI를 이용해서 유통기한 D-Day를 보여준다. fridge-web 프로젝트의 FoodCommand 객체에 다음과 같이 수정한다. public class FoodCommand { ... public static final int SHOW_EXPIRY_D_DAYS = -3; // Get/Set public int getExpiryDays() { return Period.between(LocalDate.now(), getExpiryDate()).getDays(); } public String showExpiryDDay() { if(getExpiryDays() >= SHOW_EXPIRY_D_DAYS) { if(getExpiryDays() == 0) { return "D-Day"; } else if(getExpiryDays() < 0) { return "D" + getExpiryDays(); } else if(getExpiryDays() > 0) { return "D+" + getExpiryDays(); } } return null; } // toString } SHOW_EXPIRY_D_DAYS 정적변수는 D-Day를 보여줄 기준일수를 의미한다. getExpiryDays 메서드에서는 현재날짜부터 유통기한까지의 일수(D-Day)를 가져온다. showExpiryDDay 메서드는 D-Day 를 나타내는 문자열을 만들어낸다. 다음은 뷰페이지에서 유통기한 필드를 보여주는 부분이다. <tbody> <tr

냉장고 가계부 프로젝트 24

Food 클래스의 멤버변수가 많아지면서 생성자를 이용한 객체생성이 복잡해졌다. 식품명과 수량은 반드시 입력해야 하고, 유통기한은 별도의 로직이 들어가야한다. id, fridge 필드 또한 setter 를 이용할 경우도 있고 아닐 경우도 있다. 이를 해결하고자, Food 클래스에 빌더를 추가한다. @Entity(name="food") public class Food { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @ManyToOne @JsonIgnore private Fridge fridge; private String name; private int quantity; @Temporal(TemporalType.DATE) private Date expiryDate; public static final int DEFAULT_EXPIRY_DAYS = 7; protected Food() {} public Long getId() { return id; } public Fridge getFridge() { return fridge; } public void setFridge(Fridge fridge) { this.fridge = fridge; } public String getName() { return name; } public int getQuantity() { return quantity; } public Date getExpiryDate() { return expiryDate; } private void setDefaultExpiryDate() {

냉장고 가계부 프로젝트 23

fridge-web 프로젝트에서도 개인화 작업을 진행한다. 먼저, FridgeCommand 클래스에 userId 필드를 추가한다. public class FridgeCommand { private Integer id; @NotNull @Size(min=2, max=15) private String nickname; private List<FoodCommand> foods; private Long userId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public List<FoodCommand> getFoods() { return foods; } public void setFoods(List<FoodCommand> foods) { this.foods = foods; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } } 다음은 FridgeController에서 fridge POST API를 호출하는 부분을 수정한다. @Controller @RequestMapping("/fridges") public class FridgeController { ... @P

냉장고 가계부 프로젝트 22

fridge 프로젝트의 FoodService 인터페이스의 구현클래스 JpaFoodService 의 클래스명을 일관된 네이밍을 위해서 FoodServiceImpl 클래스로 변경한다. Fridge는 냉장고를 의미하는데 아직 개인화(Personalization) 서비스는 지원하지 않고있다. 아직은 개발단계이므로 Security를 적용하면 개발 편의성이 떨어지므로 우선은 개인화 관련 컬럼을 추가해서 작업을 진행하고 추후에 security 모듈을 적용하도록 한다. 개인화 서비스는 Fridge 단계에서 식별가능한 참조컬럼만 있으면 된다. Food는 Fridge의 자식 관계를 가지므로 Fridge 모델이 개인화되면 자연스럽게 Food도 개인화영역에 들어온다. Fridge 의 개인화는 1:1 관계를 맺는다. 즉, User 한명당 하나의 Fridge 만을 가지도록 제약한다. (이제부터는 설계된 코드만 기록한다.) Fridge 클래스에 long userId 필드와 접근자/수정자를 추가한다. userId 필드는 사용자 개인을 식별할 수 있는 회원번호와 동일한 개념의 데이터를 표현한다. @Entity(name="fridge") public class Fridge { ... private Long userId; // Get/Set/Constructor public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } @Override public String toString() { return "Fridge [id=" + id + ", nickname=" + nickname + ", userId=" + userId + "]";

냉장고 가계부 프로젝트 21

fridge-web 프로젝트의 FridgeCommand 클래스에는 fridge API 서버의 HTTP통신 응답본문에 포함된 foods 컬렉션을 마샬링할 수 있도록 List<FoodCommand> 필드를 추가한다. 반대로, FoodCommand 클래스는 Fridge 필드가 추가되어서 두 클래스의 양방향 관계를 연결한다. public class FridgeCommand { ... private List<FoodCommand> foods; // Getter / Setter public List<FoodCommand> getFoods() { return foods; } public void setFoods(List<FoodCommand> foods) { this.foods = foods; } } public class FoodCommand { ... private FridgeCommand fridge; // Getter / Setter / Constructor public FridgeCommand getFridge() { return fridge; } public void setFridge(FridgeCommand fridge) { this.fridge = fridge; } // toString } Command 객체가 수정되므로 뷰파일들도 수정한다.(UI 관련해서는 github를 참고) UI 구현이 완료되면 FridgeControllerTests 클래스에 테스트 메서드를 추가한다. public class FridgeControllerTests extends ControllerBase { ... private FoodCommand food = new FoodCommand("파스퇴르 우

냉장고 가계부 프로젝트 20

Bootstrap UI를 적용해서 소스를 github로 올렸다.(UI는 변경가능성이 높기때문에 설명은 생략) fridge-web 프로젝트의 테스트 클래스 FoodControllerTests, FridgeControllerTests 클래스에서 리팩토링을 한다. API 서버의 URL을 표현하는 FOOD_API_URL, FRIDGE_API_URL 은 ControllerBase 추상클래스로 옮기고 변수명을 CORE_API_URL로 변경한다. RestTemplate 인스턴스도 자식클래스에서 동일하게 쓰이므로 추상클래스로 올린다. FoodControllerTests 클래스에서 Food 객체가 동일하게 반복되므로 픽스처로 만든다. FridgeControllerTests 클래스에서 Fridge 객체는 createFridge메서드를 이용해서 만든다. ControllerBase 추상클래스는 다음의 코드가 추가되었다. @Autowired protected RestTemplate restTemplate; protected static final String CORE_API_URL = "http://localhost:8081"; FoodControllerTests, FridgeControllerTests 클래스는 다음과 같이 변경되었다. public class FoodControllerTests extends ControllerBase { private FoodCommand food = new FoodCommand("파스퇴르 우유 1.8L", 1, new Date()); @Override protected void setUp() { restTemplate.delete(CORE_API_URL + "/foods", Collections.emptyMap()); } ... @Test public void clickAnchorTagFrom