저고데

[쇼핑몰만들기] 4. 장바구니 기능을 만들어보자 본문

쇼핑몰만들기

[쇼핑몰만들기] 4. 장바구니 기능을 만들어보자

진철 2024. 3. 11. 21:38
728x90
반응형
Entity 설계하기

우선 장바구니 기능을 만들기 위해서 Cart Entity를 설정해주었다.

장바구니는 어떤 사용자가 어떤 상품을 담았는지에 대한 정보가 있어야 하기 때문에 User와 Item을 외래키로 설계하였다.

@Getter
@Setter
@Entity
public class Cart {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;

@OneToMany
    @JoinColumn(name = "item_id")
    private Item item;
}

 

하지만 뭔가 의구심이 들었다.

한 명의 사용자가 3개의 상품을 담는다고 가정하자. (user_id는 1, item_id는 1, 2, 3으로 가정)

그러면 아래와 같이 Cart 데이터가 저장될 것이다.

id(Cart id) user_id item_id
1 1 1
1 1 2
1 1 3

뭔가 반복되는 부분이 보이지 않는가?

동일한 User가 동일한 Cart에 다른 Item을 담는 것이기 때문에 Cart_id와 User_id가 반복되는 것을 확인할 수 있다.

더군다나 이렇게 설계한다면, 초기에 User 정보를 생성할 때는 Item에 대한 정보가 없기 때문에 정규화에서 문제가 발생할 수도 있다.

따라서 Cart 내에서 담긴 Item에 대한 정보를 따로 저장하는 CartItem Entity를 만들어주고 Cart Entity를 그에 맞게 수정하였다.

@Getter
@Setter
@Entity
public class Cart {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;

}
@Getter
@Setter
@Entity
public class CartItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long count;

    @OneToOne
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne
    @JoinColumn(name = "cart_id")
    private Cart cart;
}

해당 Entity로 설계하면, User의 Cart 목록을 살펴봤을 때,

1. User의 ID로 Cart의 ID 조회

2. 조회한 Cart의 ID를 기반으로 CartItem에서 Cart의 ID와 동일한 데이터 반환

순서로 조회가 가능하다.

따라서 다음과 같이 레포지토리를 작성하였다.

@Repository
public interface CartItemRepository extends JpaRepository<CartItem, Long> {
    List<CartItem> findByCartId(Long id); // Cart_ID에 해당하는 CartItem 반환
    Optional<CartItem> findById(Long id);
}
@Repository
public interface CartRepository extends JpaRepository<Cart, Long> {
    Optional<Cart> findByUserId(Long id); //User_ID에 해당하는 Cart 반환
}

그리고 서비스 코드 역시 이를 기반으로 다음과 같이 작성하였다.

@Service
@Transactional
public class CartService {
    private final CartRepository cartRepository;

    public CartService(CartRepository cartRepository) {
        this.cartRepository = cartRepository;
    }

    public Long create(Cart cart) {
        cartRepository.save(cart);
        return cart.getId();
    }

	// User_ID에 맞는 Cart 반환
    public Optional<Cart> findByUserId(Long id) {
        Optional<Cart> cartOptional = cartRepository.findByUserId(id);
        return cartOptional;
    }
}

그리고 마지막으로 컨트롤러 코드이다.

위의 순서대로 User_ID에 해당하는 Cart_ID를 기반으로 존재하는 CartItem들을 Model을 통해서 전달해주었다.

@GetMapping("/cart")
public String findCart(Model model) {
    SessionUser user = (SessionUser) httpSession.getAttribute("user");
    if (user == null) {
        return "redirect:/login";
    }

    Optional<Cart> temp = cartService.findByUserId(user.getId());
    if (temp.isPresent()) {
        Cart cart = temp.get();
        List<CartItem> cartItems = cartItemService.findByCartID(cart.getId());
        model.addAttribute("cartItems", cartItems);
    }

    return "cart";
}
<div style="width: 80%; padding: 20px;">
    <h2>장바구니</h2>
    <div th:if="${cartItems}" style="margin-top: 1%; margin-left: 1%; overflow-y: scroll; height: 300px;">
        <ul>
            <li th:each="cartItem : ${cartItems}">
                <p th:text="${cartItem.item.name}"></p>
                <p th:text="${cartItem.item.price}"></p>
                <p th:text="${cartItem.count}"></p>
                <a th:href="@{/cart/remove/{cartItemId}(cartItemId=${cartItem.id})}" class="btn btn-danger">삭제</a>
            </li>
        </ul>
    </div>
    <div th:if!="${cartItems}" style="margin-top: 1%; margin-left: 1%; overflow-y: scroll; height: 300px;">
        <p>장바구니가 텅 비었어요</p>
    </div>
</div>
로그인 기능 수정하기

그리고 OAuth2 관련된 설정도 수정해주었다.

우선, User가 처음 생성될 때에 자동으로 Cart가 생성되는 것이기 때문에 자체 로그인에서와 소셜 로그인에서의 Cart를 구별하여 따로 생성해주었다.

먼저, 아래는 자체 로그인에서의 Cart 생성 코드이다.

Cart가 User를 외래키로 받고 있기 때문에 회원가입에서 User 생성 시, 이를 자동으로 외래키로 설정하고 생성하는 코드이다.

@PostMapping("/register")
public String create(UserForm userForm) {
    User user = new User(userForm.getName(), "null", "null", Role.USER, userForm.getUsername(), userForm.getPassword());
    userService.create(user);

    Cart cart = new Cart();
    cart.setUser(user);
    cartService.create(cart);

    return "redirect:/";
}

그리고 그 다음은 소셜 로그인에서의 Cart 생성 코드이다.

email을 바탕으로 처음 생성하는 User라면 orElseGet 메서드가 실행된다.

이 때, newUser 객체를 생성하고 해당 객체를 Cart가 외래키로 받아서 자연스레 User가 생성하는 동시에 Cart가 생성된다.

public User saveOrUpdate(OAuthAttributes attributes) {
    User user = userRepository.findByEmail(attributes.getEmail())
            .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
            .orElseGet(() -> {
                User newUser = attributes.toEntity();
                userRepository.save(newUser);

                Cart cart = new Cart();
                cart.setUser(newUser);
                cartService.create(cart);
                return newUser;
            });

    return user;
}
로그아웃을 했는데, 또 로그인이 된다 ?!

하지만 여기서 조금 어이없는 문제점을 발견하였다 !

로그아웃을 하고 다른 아이디로 로그인을 하면, 새로운 아이디가 아닌 이전의 아이디로 로그인이 된다는 점이었다.

이는 SecurityConfig에서의 역할이었기에 코드를 다시 살펴보았다.

.authorizeHttpRequests((authorizeRequest) -> authorizeRequest
        .requestMatchers("/comments/save").hasRole(String.valueOf(Role.USER))
        .requestMatchers("/login", "/register", "/login", "/upload").permitAll()
        .requestMatchers("/oauth2/authorization/google").permitAll()
        .requestMatchers("/", "/css/**", "images/**", "/js/**", "/login", "/logout/*", "/posts/**", "/comments/**").permitAll()
        .anyRequest().authenticated()
)
.logout( // 로그아웃 성공 시 / 주소로 이동
        (logoutConfig) -> logoutConfig.logoutSuccessUrl("/")
)

현재 로그인에 대한 정보는 세션을 통해서 저장된다.

하지만 맨 아래에 logout 메서드에서 로그아웃 시, 세션을 삭제하지 않고 홈 화면으로만 이동하기 때문에 발생하는 문제였다.

따라서 아래와 같이 세션을 삭제하는 메서드를 추가하여 문제를 해결하였다.

.logout(logoutConfig -> {
    logoutConfig
            .logoutSuccessHandler((request, response, authentication) -> {
                HttpSession session = request.getSession(false);
                if (session != null) {
                    session.invalidate();
                }
                response.sendRedirect("/");
            })
            .permitAll();
})
마치며

오늘은 이렇게 장바구니 기능을 추가해보고 그 과정에서 발생한 문제점을 해결해보았다.

데이터베이스에서 정규화에 대해 조금 더 알아보고 관련된 지식을 배울 수 있는 시간이었다.

웹 페이지를 설계할 때, 중요한 점은 DB에 최대한 접근하지 않도록 설계하여 비용을 줄이는 것이라고 들은 적이 있다.

오늘처럼 조금 더 생각하여 효율적인 방법으로 설계하는 습관을 들이는 것이 중요할 듯하다.

다음 시간에는 주문 기능과 즐겨찾기 기능을 추가해보도록 하겠다.

728x90
반응형