ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,48 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.core.context.SecurityContextHolder;
+import nextstep.security.exception.ForbiddenException;
+import nextstep.security.role.GrantedAuthority;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public class AuthorizationFilter extends OncePerRequestFilter {
+    private final Map<String, List<GrantedAuthority>> restrictedRoutes;
+
+    public AuthorizationFilter(Map<String, List<GrantedAuthority>> restrictedRoutes) {
+        this.restrictedRoutes = restrictedRoutes;
+    }
+
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        if (isUnauthorized(request, authentication)) {
+            throw new ForbiddenException();
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    private boolean isUnauthorized(final HttpServletRequest request, final Authentication authentication) {
+        final String requestURI = request.getRequestURI();
+
+        return restrictedRoutes.containsKey(requestURI) && isBlockedRole(authentication, requestURI);
+
+    }
+
+    private boolean isBlockedRole(final Authentication authentication, final String requestUri) {
+        List<GrantedAuthority> blockedRoles = restrictedRoutes.getOrDefault(requestUri, List.of());
+
+        return authentication.getAuthorities().stream()
+                .anyMatch(blockedRoles::contains);
+    }
+}

리뷰: 일반적으로 한 클래스 내에서 엣지 케이스를 판단할 때는 try-catch로 예외를 처리하기보다, boolean을 활용해 사전에 체크하고 핸들링하는 것이 가독성과 성능 면에서 더 좋은데요.
지금 케이스도 위 케이스에 해당하는 것 같은데 어떻게 생각하시나요?
@@ -0,0 +1,79 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationManager;
+import nextstep.security.core.context.SecurityContext;
+import nextstep.security.core.context.SecurityContextHolder;
+import nextstep.security.exception.AuthenticationException;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public abstract class AbstractAuthProcessingFilter extends OncePerRequestFilter {
+    private final AuthenticationManager authenticationManager;
+
+    protected AbstractAuthProcessingFilter(final AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        if (!match(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        if (isAlreadyAuthenticated()) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        Authentication authRequest = makeAuthentication(request);
+
+        Authentication authentication = authenticate(authRequest);
+
+        saveAuthentication(request, response, authentication);
+
+        successAuthentication(request, response, filterChain);
+
+        if (!shouldContinueFilterChain()) {
+            return;
+        }
+
+        doFilter(request, response, filterChain);
+    }
+
+    private static boolean isAlreadyAuthenticated() {
+        return SecurityContextHolder.getContext().getAuthentication() != null;
+    }
+
+    private Authentication authenticate(final Authentication authRequest) {
+        Authentication authentication = this.authenticationManager.authenticate(authRequest);
+        if (!authentication.isAuthenticated()) {
+            throw new AuthenticationException();
+        }
+
+        return authentication;
+    }
+
+    abstract boolean match(final HttpServletRequest request);
+
+    private void saveAuthentication(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) {
+        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
+        securityContext.setAuthentication(authentication);
+        SecurityContextHolder.setContext(securityContext);
+    }
+
+    abstract Authentication makeAuthentication(final HttpServletRequest request);
+
+    protected abstract void successAuthentication(final HttpServletRequest request, final HttpServletResponse response,
+                                                  final FilterChain filterChain) throws ServletException, IOException;
+
+    protected boolean shouldContinueFilterChain() {
+        return true;
+    }
+}

리뷰: 필터에서는 마지막에 doFilter()를 호출해야 정상적인 요청 흐름을 유지할 수 있습니다~
@@ -0,0 +1,19 @@
+package nextstep.security.authentication;
+
+import nextstep.security.role.GrantedAuthority;
+
+import java.util.List;
+
+public interface Authentication {
+    String getPrincipal();
+
+    String getCredentials();
+
+    boolean isAuthenticated();
+
+    List<GrantedAuthority> getAuthorities();
+
+    void addAuthority(GrantedAuthority grantedAuthority);
+
+    public boolean isNoPermission();
+}

리뷰: 애플리케이션에서 Role 을 주입시켜야 할것 같습니다 :)
@@ -0,0 +1,48 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.core.context.SecurityContextHolder;
+import nextstep.security.exception.ForbiddenException;
+import nextstep.security.role.GrantedAuthority;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public class AuthorizationFilter extends OncePerRequestFilter {
+    private final Map<String, List<GrantedAuthority>> restrictedRoutes;
+
+    public AuthorizationFilter(Map<String, List<GrantedAuthority>> restrictedRoutes) {
+        this.restrictedRoutes = restrictedRoutes;
+    }
+
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        if (isUnauthorized(request, authentication)) {
+            throw new ForbiddenException();
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    private boolean isUnauthorized(final HttpServletRequest request, final Authentication authentication) {
+        final String requestURI = request.getRequestURI();
+
+        return restrictedRoutes.containsKey(requestURI) && isBlockedRole(authentication, requestURI);
+
+    }
+
+    private boolean isBlockedRole(final Authentication authentication, final String requestUri) {
+        List<GrantedAuthority> blockedRoles = restrictedRoutes.getOrDefault(requestUri, List.of());
+
+        return authentication.getAuthorities().stream()
+                .anyMatch(blockedRoles::contains);
+    }
+}

리뷰: 네 재연님 말씀에 동의합니다 ! 
그런데 예외를 처리할때 가능한 최상위 계층에서 처리하면 비즈니스 로직에 너무많은 예외처리 코드가 포함되지 않아 더 좋은것으로 알고있는데요.
authorizationFilter에서 발생하는 인증 에러, AuthorizationFilter에서 발생하는 인가 에러 등을 처리 할때 어느 단계에서 에러처리를 하는게 좋을지 조금 어렵네요!
security코드도 filter에서 예외처리를 하고있어서 .. 가령 makeAuthentication 같은 메소드에서 요청에 대한 파라미터로 authentication객체를 만들때 예외가 발생하면 처리할 상위 처리 객체를 정하기 조금어려운거 같아요
모든 동작을 isXXX로 시작하는 메소드로 만들어서 핸들링하게 만드는건 좀 이상한거 같아서 AbstractAuthProcessingFilter에 대해서만 약간의 가이드를 주실수 있나요? 

AuthorizationFilter는 말씀해주신대로 처리했습니다!
@@ -0,0 +1,19 @@
+package nextstep.security.authentication;
+
+import nextstep.security.role.GrantedAuthority;
+
+import java.util.List;
+
+public interface Authentication {
+    String getPrincipal();
+
+    String getCredentials();
+
+    boolean isAuthenticated();
+
+    List<GrantedAuthority> getAuthorities();
+
+    void addAuthority(GrantedAuthority grantedAuthority);
+
+    public boolean isNoPermission();
+}

리뷰: 맞습니다! 핵심은 Role을 정하는 역할은 애플리케이션이 돼야 하는데요. 현재 Role은 프레임워크 레벨의 enum 타입으로 정의되어 있네요.
Role에 대한 확장성을 열어주기 위해선 Enum이 아닌 조금 더 확장성 있는 형태로 변경한다면 프레임워크 레벨이 아닌 애플리케이션 레벨에서 사용하기 좋아질 것 같습니다!
@@ -0,0 +1,48 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.core.context.SecurityContextHolder;
+import nextstep.security.exception.ForbiddenException;
+import nextstep.security.role.GrantedAuthority;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public class AuthorizationFilter extends OncePerRequestFilter {
+    private final Map<String, List<GrantedAuthority>> restrictedRoutes;
+
+    public AuthorizationFilter(Map<String, List<GrantedAuthority>> restrictedRoutes) {
+        this.restrictedRoutes = restrictedRoutes;
+    }
+
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        if (isUnauthorized(request, authentication)) {
+            throw new ForbiddenException();
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    private boolean isUnauthorized(final HttpServletRequest request, final Authentication authentication) {
+        final String requestURI = request.getRequestURI();
+
+        return restrictedRoutes.containsKey(requestURI) && isBlockedRole(authentication, requestURI);
+
+    }
+
+    private boolean isBlockedRole(final Authentication authentication, final String requestUri) {
+        List<GrantedAuthority> blockedRoles = restrictedRoutes.getOrDefault(requestUri, List.of());
+
+        return authentication.getAuthorities().stream()
+                .anyMatch(blockedRoles::contains);
+    }
+}

리뷰: > 네 재연님 말씀에 동의합니다 !
> 그런데 예외를 처리할때 가능한 최상위 계층에서 처리하면 비즈니스 로직에 너무많은 예외처리 코드가 포함되지 않아 더 좋은것으로 알고있는데요.
> authorizationFilter에서 발생하는 인증 에러, AuthorizationFilter에서 발생하는 인가 에러 등을 처리 할때 어느 단계에서 에러처리를 하는게 좋을지 조금 어렵네요!
> security코드도 filter에서 예외처리를 하고있어서 .. 가령 makeAuthentication 같은 메소드에서 요청에 대한 파라미터로 authentication객체를 만들때 예외가 발생하면 처리할 상위 처리 객체를 정하기 조금어려운거 같아요
> 모든 동작을 isXXX로 시작하는 메소드로 만들어서 핸들링하게 만드는건 좀 이상한거 같아서 AbstractAuthProcessingFilter에 대해서만 약간의 가이드를 주실수 있나요?
> 
> AuthorizationFilter는 말씀해주신대로 처리했습니다!

굉장히 중요한 내용이네요! 
`AuthorizationFilter`의 경우 해당 클래스 내에서 예외 발생 핸들링을 같이 하고 있어서 남긴 피드백이긴 합니다만... 
설계부터 접근해본다면 예외 처리를 위한 공통된 Filter를 만들고, 나머지 Filter에서는 예외를 발생시키고 예외 처리만을 위한 Filter에서만 응답을 내려주는 방식이 더 깔끔할 것 같습니다.
정답은 없고 설계에 따라 처리 방식이 많이 달라질 수 있어 어려운 내용이네요 ㅎㅎ
@@ -1,25 +1,13 @@
-import logo from './logo.svg';
-import './App.css';
+import ReviewForm from "./Components/ReviewForm";
 
 function App() {
   return (
     <div className="App">
       <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
+        <ReviewForm />
       </header>
     </div>
   );
 }
 
-export default App;
+export default App;
\ No newline at end of file

리뷰: 경로 ./Components/DetailPage/ReviewForm 요거 아닌가요  ?!
@@ -0,0 +1,52 @@
+import React from "react";
+import { useState } from "react";
+
+const ReviewForm = () => {
+    const [received, setReceived] = useState(false);
+    const [year, setYear] = useState("");
+    const [semester, setSemester] = useState("");
+    const [review, setReview] = useState("");
+
+    const Submission = () => {
+        console.log({ received, year, semester, review});
+    };
+
+    return (
+        <div>
+            <h2>리뷰쓰기</h2>
+            <div>
+                <label>
+                    <input
+                        type="checkbox"
+                        checked={received}
+                        onChange={() => setReceived(!received)}
+                    />
+                    수혜 여부
+                </label>
+
+                <input
+                    type="number"
+                    value={year}
+                    onChange={(e) => setYear(e.target.value)}
+                    placeholder="신청 연도를 입력해주세요"
+                />
+
+                <select value={semester} onChange={(e) => setSemester(e.target.value)}>
+                    <option value="">신청 학기</option>
+                    <option value="1학기">1학기</option>
+                    <option value="2학기">2학기</option>
+                </select>
+
+                <textarea
+                    value={review}
+                    onChange={(e) => setReview(e.target.value)}
+                    placeholder="리뷰를 남겨주세요"
+                />
+
+                <button onClick={Submission}>리뷰 작성하기</button>
+            </div>
+        </div>
+    );
+};
+
+export default ReviewForm;
\ No newline at end of file

리뷰: 함수 이름 파스칼케이스로 통일하셨나요? 🙂 (just 궁금)
@@ -0,0 +1,52 @@
+import React from "react";
+import { useState } from "react";
+
+const ReviewForm = () => {
+    const [received, setReceived] = useState(false);
+    const [year, setYear] = useState("");
+    const [semester, setSemester] = useState("");
+    const [review, setReview] = useState("");
+
+    const Submission = () => {
+        console.log({ received, year, semester, review});
+    };
+
+    return (
+        <div>
+            <h2>리뷰쓰기</h2>
+            <div>
+                <label>
+                    <input
+                        type="checkbox"
+                        checked={received}
+                        onChange={() => setReceived(!received)}
+                    />
+                    수혜 여부
+                </label>
+
+                <input
+                    type="number"
+                    value={year}
+                    onChange={(e) => setYear(e.target.value)}
+                    placeholder="신청 연도를 입력해주세요"
+                />
+
+                <select value={semester} onChange={(e) => setSemester(e.target.value)}>
+                    <option value="">신청 학기</option>
+                    <option value="1학기">1학기</option>
+                    <option value="2학기">2학기</option>
+                </select>
+
+                <textarea
+                    value={review}
+                    onChange={(e) => setReview(e.target.value)}
+                    placeholder="리뷰를 남겨주세요"
+                />
+
+                <button onClick={Submission}>리뷰 작성하기</button>
+            </div>
+        </div>
+    );
+};
+
+export default ReviewForm;
\ No newline at end of file

리뷰: 함수명은 보편적으로 사용하는 카멜케이스로 사용해보는 것은 어떨까요~?<br/>
추가로 `handleSubmit`과 같이 handle prefix로 수정해봐도 좋을 것 같습니다~

cf. [리액트 공식문서](https://ko.react.dev/learn/adding-interactivity)
<img width="396" alt="image" src="https://github.com/user-attachments/assets/efda3a60-12ff-4957-9e17-32377de6ce05" />
@@ -1,25 +1,13 @@
-import logo from './logo.svg';
-import './App.css';
+import ReviewForm from "./Components/ReviewForm";
 
 function App() {
   return (
     <div className="App">
       <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
+        <ReviewForm />
       </header>
     </div>
   );
 }
 
-export default App;
+export default App;
\ No newline at end of file

리뷰: 맞아요! 저런 오류가 있었네요.. 나중에 DetailPage 폴더를 추가하고 저 부분을 수정 안 한 것 같습니다 덕분에 알게 됐어요 감사합니다!!
@@ -0,0 +1,52 @@
+import React from "react";
+import { useState } from "react";
+
+const ReviewForm = () => {
+    const [received, setReceived] = useState(false);
+    const [year, setYear] = useState("");
+    const [semester, setSemester] = useState("");
+    const [review, setReview] = useState("");
+
+    const Submission = () => {
+        console.log({ received, year, semester, review});
+    };
+
+    return (
+        <div>
+            <h2>리뷰쓰기</h2>
+            <div>
+                <label>
+                    <input
+                        type="checkbox"
+                        checked={received}
+                        onChange={() => setReceived(!received)}
+                    />
+                    수혜 여부
+                </label>
+
+                <input
+                    type="number"
+                    value={year}
+                    onChange={(e) => setYear(e.target.value)}
+                    placeholder="신청 연도를 입력해주세요"
+                />
+
+                <select value={semester} onChange={(e) => setSemester(e.target.value)}>
+                    <option value="">신청 학기</option>
+                    <option value="1학기">1학기</option>
+                    <option value="2학기">2학기</option>
+                </select>
+
+                <textarea
+                    value={review}
+                    onChange={(e) => setReview(e.target.value)}
+                    placeholder="리뷰를 남겨주세요"
+                />
+
+                <button onClick={Submission}>리뷰 작성하기</button>
+            </div>
+        </div>
+    );
+};
+
+export default ReviewForm;
\ No newline at end of file

리뷰: 안그래도 네이밍컨벤션을 잘 몰라서 이번주에 공부해오기로 했습니다...ㅎㅎ 공부하고 코드 수정해보겠습니다! 두 분 다 리뷰해주셔서 감사합니다:)
@@ -0,0 +1,31 @@
+package store.domain.vo;
+
+public record Name(String value) {
+    public static final int MAX_LEN = 10;
+
+    public Name(String value) {
+        validateName(value.trim());
+        this.value = value.trim();
+    }
+
+    private void validateName(String name) {
+        if (name == null || name.isEmpty()) {
+            throw new IllegalArgumentException("[ERROR] 상품명은 공백으로 설정할 수 없습니다.");
+        }
+
+        if (name.length() > MAX_LEN) {
+            throw new IllegalArgumentException("[ERROR] 상품 명은 최대 10자까지 설정할 수 있습니다.");
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof String) {
+            return o.equals(value);
+        }
+        if (o instanceof Name) {
+            return ((Name) o).value().equals(value);
+        }
+        return false;
+    }
+}

리뷰: vo 객체로 가능한 값에 대한 검증을 내부에서 담당하는 모습이 정말 인상 깊네요! 하지만 Quantity처럼 계산 과정이 복잡해질 수 있고 계산할 때마다 새로운 객체가 생성이 되어야한다는 단점이 존재하는거로 보이는데 본인만의 적용 기준이 있을까요?? 궁금합니다!
@@ -0,0 +1,6 @@
+package store.infrastructure.constant;
+
+public class Membership {
+    public static final int DISCOUNT_RATE = 30;
+    public static final int MAX = 8_000;
+}

리뷰: 민선님의 상수화 기준이 궁금합니다! 어떤 걸 ENUM으로 공통으로 묶고 어떤 걸 내부로 관리하시나요? 또한 어떤건 하드코딩된 상태로 그대로 두시나요??
@@ -0,0 +1,54 @@
+package store.domain.vo;
+
+import store.infrastructure.constant.ExceptionMessage;
+import store.infrastructure.exception.CustomException;
+
+public record Price(int value) implements Comparable<Price> {
+    public static final int MIN = 10;
+    public static final int MAX = 1_000_000;
+
+    public Price {
+        validatePrice(value);
+    }
+
+    public static Price from(String input) {
+        try {
+            return new Price(Integer.parseInt(input.trim()));
+        } catch (NumberFormatException e) {
+            throw new CustomException(ExceptionMessage.WRONG_INTEGER_FORMAT.message());
+        }
+    }
+
+    private void validatePrice(int value) {
+        if (value < MIN || value > MAX) {
+            throw new CustomException(ExceptionMessage.WRONG_PRICE_RANGE.message());
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof Integer) {
+            return (Integer) o == value;
+        }
+        if (o instanceof Price) {
+            return ((Price) o).value == value;
+        }
+        return false;
+    }
+
+    @Override
+    public int compareTo(Price other) {
+        return value - other.value;
+    }
+
+    public int multiply(Quantity quantity) {
+        return quantity.value() * value;
+    }
+
+    public int multiply(int other) {
+        if (other <= 0) {
+            throw new IllegalStateException("0 이하의 숫자는 가격에 곱할 수 없습니다.");
+        }
+        return other * value;
+    }
+}

리뷰: 정적 팩토리 메서드 패턴은 보통 생성자를 private로 막고 메소드 명과 파라미터를 통해 그 의미를 전달하는 것으로 알고 있습니다! 이렇게 생성자와 정적 팩토리 메서드 패턴을 둘 다 사용하신 이유가 있으실까요?? 선택한 기준이 궁금합니다!
@@ -0,0 +1,31 @@
+package store.domain.vo;
+
+public record Name(String value) {
+    public static final int MAX_LEN = 10;
+
+    public Name(String value) {
+        validateName(value.trim());
+        this.value = value.trim();
+    }
+
+    private void validateName(String name) {
+        if (name == null || name.isEmpty()) {
+            throw new IllegalArgumentException("[ERROR] 상품명은 공백으로 설정할 수 없습니다.");
+        }
+
+        if (name.length() > MAX_LEN) {
+            throw new IllegalArgumentException("[ERROR] 상품 명은 최대 10자까지 설정할 수 있습니다.");
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof String) {
+            return o.equals(value);
+        }
+        if (o instanceof Name) {
+            return ((Name) o).value().equals(value);
+        }
+        return false;
+    }
+}

리뷰: 우선 좋은 질문 남겨주셔서 감사드립니다!

저도 재현님이 말씀해주신 것처럼 `vo 객체`를 사용하면서 **객체를 매번 새로 생성해야 한다는 점**, **연산 과정이 복잡해질 수 있다는 점**에서 고민을 많이 했었는데요,

아래 두 항목이 가져다주는 이점이 더 크다고 생각되는 부분에는 `vo 객체`를 도입했습니다.
```
1. 검증 위치를 한 곳으로(객체 내부로) 통일시킬 수 있다.
2. 불변성을 보장하여 안정적으로 값을 관리할 수 있다.
```

예를들어 `상품명`의 경우에는 출력 포맷에 영향을 줄 수 있기 때문에, `상품명 길이 제한`이 필요하다고 생각했어요.
이러한 `상품명에 대한 검증 요구사항`은 언제든지 변동될 수 있기 때문에, **validation 과정**을 외부에 두는 것보다는 **값 자체를 객체로 관리해서 관리 지점을 통일시키고자** 했습니다.

또한, 상품명은 **서비스가 동작하는 동안에는 변동될 일이 없기** 때문에, `불변성`을 보장한다는 측면에서도 별도의 객체로 관리하는 것이 안정적일 것이라고 생각했습니다