본문 바로가기

프로젝트 이야기/물품 지급앱

1. 핵심 기능 구현(1) - 도메인 설계

요구 기능 사항

1. 유저 이름 변경
2. 물품 지급

 

각 기능의 비즈니스 로직은 아래와 같습니다.

 

  1. 유저 이름 변경
    • 닉네임으로 유저 조회
    • 변경할 닉네임 입력
    • 변경할 닉네임이 이미 존재하는 닉네임인지 조회
    • 닉네임 변경
  2. 물품 지급
    • 물품을 지급하는 유저 조회
    • 물품을 받을 유저 조회
    • 지급일 입력
    • 물품 검색
    • 지급할 물품 조회
    • 메모 입력
    • 물품 지급

도메인

3개의 도메인으로 구성됩니다. 유저(User)와 물품(Item)은 이름과 동일하게 유저와 물품의 정보를 저장하는 엔티티입니다. 선물(Gift)는 물품을 지급했을 때, 발신자, 수신자, 물품, 개수 등을 저장하는 지급장부입니다.

 

  • 유저 (Userinfo)
    • 야이디 : id
    • 유저명 : nickname
    • 생성시간 : insertTime
    • UID
  • 물품 (Item)
    • 아이디 : id
    • 물품명 : name
  • 선물 (Gift)
    • 발신자 아이디 : sendUserId
    • 발신자 이름 : sendUserNickname
    • 수신자 아이디 : receiveUserId
    • 물품 아이디 : giftId
    • 지급 날짜 : sendDate
    • 만료 날짜 : expiredDate
    • 메모 : memo

 

사실 엔티티 필드를 살펴보면 알겠지만 외래키(foreign key)를 이용해 설계시, 더 효율적으로 DB를 설계할 수 있는 부분이 많습니다... 하지만 이미 서비스를 운영중인 디비로 한낱 개발자인 제가 건드려서는 안돼는 부분입니다. 일단은 이미 설계된 테이블에 맞게 엔티티만 매핑하였습니다.

 

위에서 정리한 비지니스 로직을 도메인 별로 다시 정리해 보겠습니다.

 

  • 유저 (Userinfo)
    • 유저 조회 (닉네임)
    • 유저 닉네임 변경
  • 물품 (Item)
    • 물품 조회 (닉네임)
    • 물품 전체 조회
  • 지급장부 (Gift)
    • 장부 생성

 


Userinfo

 

1. 엔티티 설계

 

Userinfo.java

@Entity  
@Getter 
@NoArgsConstructor  
@Table(name = "userinfo")  
public class UserInfo {  

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)  
    @Column(name = "user_id")  
    private Long id;  

    @Column(name = "insert_time")  
    private LocalDateTime insertTime;  

    @Column(name = "uid")  
    private String UID;  

    @Column(name = "nickname")  
    private String nickname;  

    //== 생성메서드 ==// 
    public static UserInfo createUserInfo(String UID, String nickname) {  
        UserInfo userInfo = new UserInfo();  
        userInfo.insertTime = LocalDateTime.now();  
        userInfo.UID = UID;  
        userInfo.nickname = nickname;

        return userInfo;  
    }  

    //== 비지니스 메서드 ==//    
    public void setAll(String UID, String nickname) {  
        this.UID = UID;  
        this.nickname = nickname;  
    }  

}

@Table과 @Column 어노테이션을 통해 기존 테이블의 정보와 엔티티를 매핑합니다.

이미 운영중인 데이터베이스에 주요 키(primary key) 생성을 위한 시퀀스(sequence)가 정의되어 있으므로 기본키 생성에 대한 전략은 데이터베이스에게 위임합니다. 이를 구현하기위해 @GenerateValue 어노테이션의 전략(starategy)를 IDENTITY로 지정해줍니다.

생성메서드는 추후 유저 생성 기능을 위해 미리 구현해주었습니다.

 

 

2. 레포지토리 설계

 

UserRepository.java

@Repository  
public class UserInfoRepository {  

    @PersistenceContext  
    EntityManager em;  

    public Long save(UserInfo userInfo) {  
        em.persist(userInfo);  
        return userInfo.getId();  
    }  

    public UserInfo findOne(Long id) {  
        return em.find(UserInfo.class, id);  
    }  

    public UserInfo findOneByNickname(String nickName) {  
        List<UserInfo> resultList = em.createQuery("SELECT u FROM UserInfo u WHERE u.nickname = :nickname", UserInfo.class)  
                .setParameter("nickname", nickName)  
                .getResultList();  

        if (resultList.size() == 0) return null;  
        return resultList.get(0);  
    }  

    public List<UserInfo> findAll() {  
        return em.createQuery("SELECT u FROM UserInfo u", UserInfo.class)  
                .getResultList();  
    }  
}

기본적인 저장, 단건조회, 전체조회 기능을 구현했습니다. 그리고 비지니스 로직에서 요구했던 닉네임을 통해 유저를 조회하는 메서드(findOneByNickaname)도 구현했습니다.

 

 

3. 서비스 설계
@Service  
@Transactional 
@RequiredArgsConstructor  
public class UserInfoService {  

    private final UserInfoRepository userInfoRepository;  

    public UserInfo create(String UID, String nickname) {  
        UserInfo createdUserInfo = UserInfo.createUserInfo(UID, nickname);  
        userInfoRepository.save(createdUserInfo);  
        return createdUserInfo;  
    }  

    public UserInfo editUserNickname(String nickname, String newNickname) {  
        UserInfo findUser = userInfoRepository.findOneByNickname(nickname);  
        findUser.setNickname(newNickname);  

        return findUser;  
    }  

    public UserInfo update(Long id, UserInfoDto dto) {  
        UserInfo findUser = userInfoRepository.findOne(id);  
        findUser.setAll(dto.getUID(), dto.getNickname(), dto.getOption());  

        return findUser;  
    }  
}

create 메서드는 Userinfo를 생성하여 데이터베이스에 저장합니다. 유저정보(UID, nickname)를 받아 Userinfo의 정적메서드인 createUserInfo를 통해 UserInfo 객체를 생성한다. 그리고 레포지토리 저장 메서드를 호출하여 생성한 객체를 데이터베이스에 저장한다.

editUserNickname 메서드는 유저를 조회하여 유저의 닉네임을 변경합니다. 현재의 닉네임과 변경할 닉네임을 매게변수로 받아, 현재의 닉네임으로 유저를 조회한 후 닉네임을 setter 메서드를 통해 변경합니다.


 

Item

물품은 단순 조회만 함으로 구현이 매우 간단합니다.

 

1. 엔티티

 

Item.java

@Entity  
@Getter @Setter  
@Table(name = "item")  
public class Item {  

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "item_id")  
    private Long id;  

    @Column(name = "item_name")  
    private String name;  
}

엔티티는 필드값으로 식별자 아이디(primary_key)와 물품명(name)을 갖는다. 주요 키의 생성 전략은 데이터베이스에 위임한다.

 

 

2. 레파지토리

 

ItemRepository.java

@Repository  
public class ItemRepository {  

    @PersistenceContext  
    EntityManager em;  

    public Item findOne(Long id) {  
        return em.find(Item.class, id);  
    }  

    public Item findOneByName(String name) {  
        List<Item> resultList = em.createQuery("select i from Item i where i.name = :name", Item.class)  
                .setParameter("name", name)  
                .getResultList();  
        if (resultList.size() == 0) return null;  
        else return resultList.get(0);  
    }  

    public List<Item> findAll() {  
        return em.createQuery("select i from Item i", Item.class)  
                .getResultList();  
    }  
}

조회메서드만 구현하였다. 각각 단건조회, 단건조회(닉네임), 전체 조회이다.

 

 

3. 서비스

물품 엔티티는 조회 기능외에는 수행하는 것이 없으므로 레파지토리의 메서드를 직접 사용해도 큰 문제가 없다고 판단되어 서비스를 구현하지 않았다.


 

Gift

지급 장부 도메인은 생성 기능 외에는 구현할 기능이 없다. 생성 기능 구현을 위주로 개발하였다.

 

 

1. 도메인

 

Gift.java

@Entity  
@Getter @Setter  
@Table(name = "gift")  
public class Gift {  

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "gift_id")  
    private Long id;  

    @Column(name = "senduser_id")  
    private Long sendUserId;  

    @Column(name = "senduser_nickname")  
    private String sendUserNickname;  

    @Column(name = "receiveuser_id")  
    private Long receiveUserId;  

    @Column(name = "item_id")  
    private Long giftItemId;  

    @Column(name = "notified")  
    private Boolean notified;  

    @Column(name = "send_date")  
    private LocalDateTime sendDate;  

    @Column(name = "memo")  
    private String memo;  

    @Column(name = "expire_date")  
    private LocalDateTime expireDate;  


    //== 생성 메서드 ==//  
    public static Gift createGift(Long sendUserId, Long receiveUserId, Long giftItemId, LocalDateTime expireDate, String memo) {  
        Gift gift = new Gift();  
        gift.sendUserId = sendUserId;  
        gift.receiveUserId = receiveUserId;  
        gift.giftItemId = giftItemId;  
        gift.notified = false;  
        gift.sendDate = LocalDateTime.now();  
        gift.memo = memo;  
        gift.expireDate = expireDate;  
        return gift;  
    }  
}

생성 기능을 구현할 것이므로 테이블의 모든 칼럼을 매핑해주어야 합니다. 또한 생성 메서드를 정적 메서드로 구현해 서비스 코드에서 사용하도록 하겠습니다.

 

 

2. 레파지토리

 

GiftRepository.java

@Repository  
public class GiftRepository {  

    @PersistenceContext  
    EntityManager em;  

    public Long save(Gift gift) {  
        em.persist(gift);  
        return gift.getId();  
    }  

    public Gift findOne(Long id) {  
        return em.find(Gift.class, id);  
    }  
}

저장을 위한 save 메서드와 단건조회 메서드를 구현하였습니다.

 

 

3. 서비스

 

GiftService.java

@Service  
@Transactional(readOnly = true)  
@RequiredArgsConstructor  
public class GiftService {  

    private final GiftRepository giftRepository;  
    private final UserInfoRepository userInfoRepository;  
    private final ItemRepository itemRepository;  

    @Transactional  
    public Gift create(Long sendUserId, Long receiveUserId, Long giftItemId, LocalDateTime expireDate, String memo) {  

        Gift gift = Gift.createGift(sendUserId, receiveUserId, giftItemId, expireDate, memo);  
        String sendUserNickname = userInfoRepository.findOne(sendUserId).getNickname();  
        gift.setSendUserNickname(sendUserNickname);  

        giftRepository.save(gift);  

        return gift;  
    }  

    @Transactional  
    public Gift create(String sendUserNickname, String receiveUserNickname, String giftItemName, String memo, LocalDateTime expireDate) {  

        Long sendUserId = (sendUserNickname.equals("_system")) ?  
                3400L : userInfoRepository.findOneByNickname(sendUserNickname).getId();  
        Long receiveUserId = userInfoRepository.findOneByNickname(receiveUserNickname).getId();  
        Long giftItemId = itemRepository.findOneByName(giftItemName).getId();  

        Gift gift = Gift.createGift(sendUserId, receiveUserId, giftItemId, expireDate, memo);  
        gift.setSendUserNickname(sendUserNickname);  
        if (sendUserNickname.equals("_system")) gift.setSendUserNickname("");  

        giftRepository.save(gift);  

        return gift;  
    }  

}

GiftService에서 Gift 객체를 생성하여 데이터베이스에 저장하는 메서드가 구현되어 있습니다. 두 개의 메서드로 오버로딩(overloading)이 되어 있는데 첫번째 메서드는 아이디 값을 기반으로 매개변수를 받고, 후자는 이름 값을 기반으로 매개변수를 받습니다.

 

만약에 발신자가 시스템 계정일 경우, 발신자의 이름이 '_system'이 되게 됩니다. 이 경우, 아이디 값을 3400(시스템 계정의 아이디)으로 설정해줍니다.

 

두 메서드 모두 Gift의 정적 생성 메서드를 통해 객체를 생성후 레파지토리의 save 메서드를 통해 데이터베이스에 저장함으로써 지급 장부를 생성합니다.

 

 


 

이로써 모든 도메인 설계가 마무리 되었습니다. 이제 이 도메인을 기반으로 REST API를 설계해야 합니다.
REST API를 설계하는 과정은 2편에서 이어 가겠습니다!!