기본 콘텐츠로 건너뛰기

라벨이 가계부인 게시물 표시

냉장고 가계부 프로젝트 40

식품을 추가하면, 해당 식품이 어떤 종류인지 사람은 쉽게 유추할 수 있다. 예를들어, 우유, 치즈 같은 제품은 유제품으로 분류할 수 있고, 사과, 바나나, 딸기 같은 제품은 과일로 분류할 수 있다. 이렇게 같은 특성을 지닌 부류나 범위를 범주 , 또는 카테고리 라고 한다. 사용자가 새로운 식품을 추가하면, 해당 식품이 어느 범주에 속하는지를 분류해서 입력값으로 넘겨준다면 좋겠지만, 너무 많은 입력을 요구하면 View가 복잡해지고 사용자 편의성이 떨어지게 된다. 따라서, 입력되는 식품명을 기반으로 해당 식품의 범주가 어디에 속하는지 분류해준다면 편리할 것이다. 식품명에 따른 범주화해주는 로직은 다음과 같다. 사용자로부터 식품명을 입력받는다. 사전에 분류해둔 키워드 데이터들과 입력받은 식품명의 유사도를 측정한다. 측정된 유사도 결과값 중에서 가장 유사한 키워드의 범주를 반환한다. 예를들어, {사과, 바나나, 딸기} 는 "과일" 이라는 집단으로 분류하고, {우유, 치즈, 요거트} 는 "유제품" 이라는 집단으로 분류한다. 사용자가 입력한 식품명이 "맛있는 사과 5kg (23과내)" 라고 주어졌을 때, "사과" - "맛있는 사과 5kg (23과내)" 와 유사도를 측정하고, "바나나" - "맛있는 사과 5kg (23과내)" 와 유사도를 측정하고, "딸기" - "맛있는 사과 5kg (23과내)" 와 유사도를 측정한다. 같은 방식으로 유제품의 키워드들과도 유사도를 측정한다. 키워드 각각 주어진 식품명과 유사도를 측정해서 나온 결과 값이 가장 높은 키워드의 범주값을 반환한다. 키워드 데이터와 주어진 문자열 사이의 유사한 정도를 측정하기 위해서 자카드 유사도 라는 알고리즘을 사용한다. 자카드 유사도( 자카드 지수 )는 두 집합 사이의 유사도를 측정하는 방법이다. 자카드 유사도 알고...

냉장고 가계부 프로젝트 39

식품명을 등록하는 부분에 자동완성 기능을 제공하면 사용자가 식품명을 입력할 때 좀 더 편리하게 사용할 수 있다. 위 스크린샷처럼 식품명을 한글자씩 입력할 때마다, 입력한 키워드로 시작하는 단어들을 제공한다. 냉장고 가계부 프로젝트에서는 단지 식품명을 입력하는데 도움을 주는 기능을 제공하므로, 사전에 준비된 키워드 목록을 제공한다. javascript는 bootstrap3-typeahead 를 사용한다. 키워드를 검색하기 위한 fridge-search 프로젝트를 새로 생성한다. 키워드 목록의 빠른 검색을 위해 Trie 알고리즘 을 사용한다. Trie 알고리즘을 구현한 Apache Commons Collection4 라이브러리를 사용하기위해 pom.xml 에 다음과 같이 의존성을 추가한다. <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> </dependency> Apache Commons Collections의 Trie  인터페이스를 구현한 구현클래스 PatriciaTrie 를 사용하여 자동완성 기능을 구현한다. 내부적으로 Map 인터페이스를 구현하고 있기때문에 데이터의 추가는 put, 삭제는 remove 메서드를 사용하며, prefixMap 이라는 메서드는 주어진 key 값을 접두사로 갖는 데이터들을 SortedMap 타입으로 리턴한다. 식품명 자동완성을 위해 ProductNameTrie 클래스를 다음과 같이 구현했다. @Component public class ProductNameTrie { private Trie<String, Long> trie = new PatriciaTrie<>(); private static final int MAX_SIZE = 10; publ...

냉장고 가계부 프로젝트 38

Fridge 프로젝트의 Fridge, Food Entity 클래스의 @CreatedDate, @LastModifedDate 컬럼이 중복되어 나타나므로 상위클래스로 옮긴다. 상위 클래스는 BaseEntity 클래스로 선언하고 두개의 멤버 변수를 선언한다. Fridge, Food 클래스는 BaseEntity 를 상속받는다. @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public class BaseEntity { private @CreatedDate LocalDateTime createdDate; private @LastModifiedDate LocalDateTime lastModifiedDate; } Food 클래스는 다음과 같이 클래스 애너테이션이 선언된다. @NoArgsConstructor @Getter @Setter @ToString @Entity public class Food extends BaseEntity { ... } lombok @Getter, @Setter, @ToString 애너테이션을 선언하고 LocalDateTime createDate, LocalDateTime lastModifiedDate 필드는 제거한다. Fridge 클래스도 동일하게 수정한다. UI 서버에서 로그인을 한 뒤에, 비밀번호 변경과 회원탈퇴 기능은 제공되지만, 비밀번호를 잊어버렸을 경우 로그인을 할 수 없는 문제가 있다. 따라서, 비밀번호 찾기 기능을 제공한다. 비밀번호 찾기 기능은 가입시 입력한 이메일을 입력하면 해당 이메일로 인증코드가 발송되고, 해당 메일에서 인증코드 링크를 클릭하면 비밀번호를 변경할 수 있는 페이지를 제공한다. 인증코드는 Member 서비스에서 관리하는게 적합한데, 개별 회원정보가 Member 서비스에서 관리되기 때문이다. Member 프로젝트의 스키마를 수정한다. CREATE TABLE member ( id BIGIN...

냉장고 가계부 프로젝트 37

fridge 프로젝트도 member 프로젝트와 같이 Spring Data Rest 를 사용하여 코드를 정리한다. response 본문에 id 가 표시되도록 RepositoryRestConfig 클래스를 작성한다. @Configuration public class RepositoryRestConfig extends RepositoryRestConfigurerAdapter { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { config.exposeIdsFor(Fridge.class, Food.class); } } Fridge, Food 클래스 두 엔티티의 Id를 노출한다. FridgeRepository, FoodRepository 인터페이스에 @RepositoryRestResource 애너테이션을 선언한다. @RepositoryRestResource public interface FridgeRepository extends JpaRepository<Fridge, Integer> { Optional<Fridge> findByUserId(@Param("userId") Long userId); } @RepositoryRestResource public interface FoodRepository extends JpaRepository<Food, Long> { } FridgeRepository의 findByUserId 메서드의 파라미터에 @Param("userId") 애너테이션을 선언한다. findByUserId 메서드는 fridges/search/findByUserId GET URL 형태로 제공된다. @Param("userId") 를 선언하면 QueryString userId를 키로 사용...

냉장고 가계부 프로젝트 36

member 프로젝트에서는 간단한 CRUD 기능만을 가지므로 Spring Data Rest를 사용해서 코드의 양을 줄일 수 있다. Spring Data Rest와 Spring HATEOAS, Spring Data JPA 를 사용한다. pom.xml 에 Spring Data Rest 의존성을 추가한다. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> Spring Data Rest는 Repository인터페이스에 @RepositoryRestResource 애너테이션을 추가하기만 하면 CRUD 관련 endpoint를 작성해준다. MemberRepository 인터페이스에 다음과 같이 애너테이션을 추가한다. @RepositoryRestResource public interface MemberRepository extends JpaRepository<Member, Long> { Optional<Member> findByUsername(String username); } Spring Data Rest는 {repository} GET,POST {repository}/{id} GET,PUT,PATCH,DELETE, {repository}/search GET 과 같은 CRUD endpoint를 작성해준다. MemberRepository는 members, members/{id}, members/search 와 같다. (더 상세한 내용은  Spring Data Rest Reference  를 참고.) Spring Data Rest 관련 설정은 yaml 을 통해서도 가능하고, 별도의 Configuration 클래스를 작성할 수도 있다. Member 프로젝트같은 경우 ...

냉장고 가계부 프로젝트 35

Spring security 프레임워크에서 form login 방식으로 인증할 경우 Remember-Me 서비스를 제공할 수 있다. Remember-Me 서비스를 이용하면 쿠키에 인증토큰을 저장해두고 일정기간동안(default: 2주) 쿠키에 토큰이 있다면 인증처리를 해준다. 기본적으로 별도의 설정이 없다면 세션은 30분 만료시간으로 정해져있다.(WAS 마다 틀리다.) 세션 시간이 만료되면 세션에 저장된 정보가 날라가므로 다시 로그인을 해야한다. Remeber-Me 기능을 이용하면, form login에서 인증을 성공적으로 진행하는 경우 임의의 인증토큰을 생성해서 쿠키에 저장해두고 쿠키가 만료되기 전까지 쿠키값을 확인해서 인증을 진행한다. 별도의 설정을 하지 않으면 in-memory 기반의 쿠키저장소를 이용해서 쿠키 토큰인증을 하며, JDBC같은 저장소를 별도로 사용할 수도 있다. WebSecurityConfig 클래스에 다음과 같이 간단한 설정만으로 remember-me 기능을 추가할 수 있다. @Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final MemberUserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/signup").permitAll() .requestMatchers(PathRequest.toStaticResources().atCommonLocations()...

냉장고 가계부 프로젝트 34

회원가입과 로그인, 로그아웃 기능은 구현했으니, 이제는 비밀번호 변경과 회원탈퇴기능을 구현한다. member 프로젝트의 MemberService 클래스에 탈퇴 메서드를 추가한다. @Service @Transactional @RequiredArgsConstructor public class MemberService { private final MemberRepository repository; public Member save(Member member) { return repository.save(member); } public void withdraw(long id) { repository.deleteById(id); } } withdraw 메서드는 파라미터로 회원 아이디를 입력받아서 repository의 deleteById 메서드를 호출한다. MemberController 클래스에서 비밀번호 변경, 회원탈퇴 endpoint를 작성한다. @RestController @RequestMapping("/members") @RequiredArgsConstructor public class MemberController { ... @PutMapping("/{id}") ResponseEntity<?> changeMemberPassword(@PathVariable final long id, @RequestBody final Member memberRequest) throws URISyntaxException { Member updatedMember = repository.findById(id) .map(member -> { member.setPassword(memberRequest.getPassword()); ...

냉장고 가계부 프로젝트 33

Spring security 의 권한 구분과 계정잠금/만료 정책관련 필드 정보를 제공하기 위해 Member 서버를 수정한다. CREATE TABLE member ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) PRIMARY KEY, username VARCHAR(256) NOT NULL, password VARCHAR(128) NOT NULL, account_non_expired BOOLEAN NOT NULL DEFAULT TRUE, account_non_locked BOOLEAN NOT NULL DEFAULT TRUE, credentials_non_expired BOOLEAN NOT NULL DEFAULT TRUE, enabled BOOLEAN NOT NULL DEFAULT TRUE ); ALTER TABLE member ADD CONSTRAINT UK_member_username UNIQUE (username); CREATE TABLE member_authorities ( member_id BIGINT NOT NULL, authority VARCHAR(32) NOT NULL ); member 테이블에는 account_non_expired, account_non_locked, credentials_non_expired, enabled 네 개의 Boolean 컬럼이 추가되고 Default 값은 true로 지정한다. 그리고 권한구분값을 저장할 member_authorities 테이블을 생성한다. member_authorities 테이블에는 member 테이블의 식별키 id값을 참조하는 member_id 컬럼과 문자열로 저장되는 권한구분 authority 컬럼으로 구성된다. @Data @NoArgsConstructor @Entity public class Member { @Id @Gene...

냉장고 가계부 프로젝트 32

fridge-member 프로젝트의 회원 가입을 위한 API를 추가한다. UI 서버에서 회원가입을 진행하면 아이디와 패스워드가 요청바디에 담겨서 member API를 POST 방식으로 호출한다. JPA repository 의 save 메서드를 호출하는 서비스 로직을 담는 MemberService 클래스를 우선 작성한다. @Service @Transactional @RequiredArgsConstructor public class MemberService { private final MemberRepository repository; public Member save(Member member) { return repository.save(member); } } 클래스에는 Service 애너테이션이 선언되고, Transactional 애너테이션이 선언되어 있다. Lombok 라이브러리의 RequiredArgsConstructor 애너테이션을 선언해서 repository 의존성을 생성자에서 주입한다. save 메서드는 Member 클래스를 파라미터로 전달받고 생성된 Member 객체를 리턴한다. 메서드 내부에서는 repository 의 save메서드를 호출한다. UI 서버에서 호출하는 endpoint인 컨트롤러 메서드를 MemberController 클래스에 추가한다. @RestController @RequestMapping("/members") @RequiredArgsConstructor public class MemberController { private final MemberRepository repository; private final MemberResourceAssembler assembler; private final MemberService service; ... @PostMapping ResponseEn...