ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,87 @@
+package nextstep.security.authorization.role;
+
+import nextstep.security.SimpleGrantedAuthority;
+import nextstep.security.authorization.GrantedAuthority;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class RoleHierarchyTest {
+
+    @Test
+    void getReachableGrantedAuthorities() {
+        RoleHierarchy roleHierarchy = RoleHierarchyImpl.fromHierarchy(
+                """
+                ROLE_ADMIN > ROLE_USER
+                ROLE_USER > ROLE_GUEST
+                """
+        );
+
+        List<String> reachableAuthorities =
+                roleHierarchy.getReachableGrantedAuthorities(Set.of(new SimpleGrantedAuthority("ROLE_ADMIN")))
+                        .stream().map(GrantedAuthority::getAuthority).toList();
+
+        assertThat(reachableAuthorities).containsExactlyInAnyOrder(
+                "ROLE_ADMIN",
+                "ROLE_USER",
+                "ROLE_GUEST"
+        );
+
+    }
+
+    @Test
+    void getComplexReachableGrantedAuthorities() {
+        RoleHierarchy roleHierarchy = RoleHierarchyImpl.fromHierarchy(
+                """
+                ROLE_ADMIN > ROLE_USER
+                ROLE_USER > ROLE_GUEST
+                ROLE_ADMIN > ROLE_ANY
+                """
+        );
+
+        List<String> reachableAuthorities =
+                roleHierarchy.getReachableGrantedAuthorities(Set.of(new SimpleGrantedAuthority("ROLE_ADMIN")))
+                        .stream().map(GrantedAuthority::getAuthority).toList();
+
+        assertThat(reachableAuthorities).containsExactlyInAnyOrder(
+                "ROLE_ADMIN",
+                "ROLE_USER",
+                "ROLE_GUEST",
+                "ROLE_ANY"
+        );
+
+    }
+
+    @Test
+    void buildWithDsl() {
+        RoleHierarchy roleHierarchy = RoleHierarchyImpl.with()
+                .role("ROLE_ADMIN").implies("ROLE_USER")
+                .role("ROLE_USER").implies("ROLE_GUEST")
+                .build();
+
+        List<String> reachableAuthorities =
+                roleHierarchy.getReachableGrantedAuthorities(Set.of(new SimpleGrantedAuthority("ROLE_ADMIN")))
+                        .stream().map(GrantedAuthority::getAuthority).toList();
+
+        assertThat(reachableAuthorities).containsExactlyInAnyOrder(
+                "ROLE_ADMIN",
+                "ROLE_USER",
+                "ROLE_GUEST"
+        );
+    }
+
+    @Test
+    void hierarchyCycleDetect() {
+        assertThatThrownBy(() -> RoleHierarchyImpl.with()
+                .role("ROLE_ADMIN").implies("ROLE_USER")
+                .role("ROLE_USER").implies("ROLE_GUEST")
+                .role("ROLE_GUEST").implies("ROLE_ADMIN") // Cycle
+                .build())
+                .isInstanceOf(CycleInRoleHierarchyException.class);
+    }
+}

리뷰: 테스트 너무 좋네요 👍 
추가로 순환 참조가 발생했을 때와 같은 예외 상황에 대한 테스트도 추가해보면 어떨까요?
@@ -0,0 +1,87 @@
+package nextstep.security.authorization.role;
+
+import nextstep.security.SimpleGrantedAuthority;
+import nextstep.security.authorization.GrantedAuthority;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class RoleHierarchyTest {
+
+    @Test
+    void getReachableGrantedAuthorities() {
+        RoleHierarchy roleHierarchy = RoleHierarchyImpl.fromHierarchy(
+                """
+                ROLE_ADMIN > ROLE_USER
+                ROLE_USER > ROLE_GUEST
+                """
+        );
+
+        List<String> reachableAuthorities =
+                roleHierarchy.getReachableGrantedAuthorities(Set.of(new SimpleGrantedAuthority("ROLE_ADMIN")))
+                        .stream().map(GrantedAuthority::getAuthority).toList();
+
+        assertThat(reachableAuthorities).containsExactlyInAnyOrder(
+                "ROLE_ADMIN",
+                "ROLE_USER",
+                "ROLE_GUEST"
+        );
+
+    }
+
+    @Test
+    void getComplexReachableGrantedAuthorities() {
+        RoleHierarchy roleHierarchy = RoleHierarchyImpl.fromHierarchy(
+                """
+                ROLE_ADMIN > ROLE_USER
+                ROLE_USER > ROLE_GUEST
+                ROLE_ADMIN > ROLE_ANY
+                """
+        );
+
+        List<String> reachableAuthorities =
+                roleHierarchy.getReachableGrantedAuthorities(Set.of(new SimpleGrantedAuthority("ROLE_ADMIN")))
+                        .stream().map(GrantedAuthority::getAuthority).toList();
+
+        assertThat(reachableAuthorities).containsExactlyInAnyOrder(
+                "ROLE_ADMIN",
+                "ROLE_USER",
+                "ROLE_GUEST",
+                "ROLE_ANY"
+        );
+
+    }
+
+    @Test
+    void buildWithDsl() {
+        RoleHierarchy roleHierarchy = RoleHierarchyImpl.with()
+                .role("ROLE_ADMIN").implies("ROLE_USER")
+                .role("ROLE_USER").implies("ROLE_GUEST")
+                .build();
+
+        List<String> reachableAuthorities =
+                roleHierarchy.getReachableGrantedAuthorities(Set.of(new SimpleGrantedAuthority("ROLE_ADMIN")))
+                        .stream().map(GrantedAuthority::getAuthority).toList();
+
+        assertThat(reachableAuthorities).containsExactlyInAnyOrder(
+                "ROLE_ADMIN",
+                "ROLE_USER",
+                "ROLE_GUEST"
+        );
+    }
+
+    @Test
+    void hierarchyCycleDetect() {
+        assertThatThrownBy(() -> RoleHierarchyImpl.with()
+                .role("ROLE_ADMIN").implies("ROLE_USER")
+                .role("ROLE_USER").implies("ROLE_GUEST")
+                .role("ROLE_GUEST").implies("ROLE_ADMIN") // Cycle
+                .build())
+                .isInstanceOf(CycleInRoleHierarchyException.class);
+    }
+}

리뷰: 생성시에 탐지하는 것이 좋을 것 같아 생성자쪽에 추가해보았습니다!
@@ -1,20 +1,22 @@
 package nextstep.security.authorization;
 
 import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.hierarchy.RoleHierarchy;
 
 import java.util.Set;
 
 public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
-    private final AuthoritiesAuthorizationManager delegate = new AuthoritiesAuthorizationManager();
+    private final AuthoritiesAuthorizationManager delegate;
 
-    private final String authority;
+    private final Set<String> authorities;
 
-    public AuthorityAuthorizationManager(String authority) {
-        this.authority = authority;
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy, String ...authorities) {
+        this.delegate = new AuthoritiesAuthorizationManager(roleHierarchy);
+        this.authorities = Set.of(authorities);
     }
 
     @Override
     public AuthorizationDecision check(final Authentication authentication, final T object) {
-        return this.delegate.check(authentication, Set.of(this.authority));
+        return this.delegate.check(authentication, this.authorities);
     }
 }

리뷰: Spring Security의 AuthoritiesAuthorizationManager는 String... authorities를 사용하여 다중 권한을 처리할 수 있도록 설계되었습니다.
가변인자 활용을 통해 AuthorityAuthorizationManager가 더 유연하게 동작할 수 있도록 해보면 어떨까요? 😄 
```suggestion
    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy, String... authorities) {
```
@@ -1,20 +1,22 @@
 package nextstep.security.authorization;
 
 import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.hierarchy.RoleHierarchy;
 
 import java.util.Set;
 
 public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
-    private final AuthoritiesAuthorizationManager delegate = new AuthoritiesAuthorizationManager();
+    private final AuthoritiesAuthorizationManager delegate;
 
-    private final String authority;
+    private final Set<String> authorities;
 
-    public AuthorityAuthorizationManager(String authority) {
-        this.authority = authority;
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy, String ...authorities) {
+        this.delegate = new AuthoritiesAuthorizationManager(roleHierarchy);
+        this.authorities = Set.of(authorities);
     }
 
     @Override
     public AuthorizationDecision check(final Authentication authentication, final T object) {
-        return this.delegate.check(authentication, Set.of(this.authority));
+        return this.delegate.check(authentication, this.authorities);
     }
 }

리뷰: Set 으로 중복을 제거해 주셨군요 👍
@@ -1,8 +1,8 @@
 export type User = {
   id: number;
-  email: string;
+  email?: string;
   name: string;
-  companyName: string;
+  companyName?: string;
   image: string | null;
 };
 
@@ -13,3 +13,52 @@ export type GatheringParticipant = {
   joinedAt: string;
   User: User;
 };
+
+export type ReviewQuery = {
+  gatheringId?: string;
+  useId?: number;
+  type?: string;
+  location?: string;
+  date?: string;
+  registrationEnd?: string;
+  sortBy?: "createdAt" | "score" | "participantCount";
+  sortOrder?: "asc" | "desc";
+  limit?: number;
+  offset?: number;
+};
+
+type Gathering = {
+  teamId: string;
+  id: number;
+  type: string;
+  name: string;
+  dateTime: string;
+  location: string;
+  image: string;
+};
+
+export type Review = {
+  teamId?: string;
+  id: number;
+  score: number;
+  comment: string;
+  createdAt: string;
+  Gathering?: Gathering;
+  User: User;
+};
+
+// 실제 데이터 응답값
+export type ReviewsResponse = {
+  data: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};
+
+// 리뷰 정제 데이터
+export type ReviewData = {
+  reviews: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};

리뷰: _:hammer_and_wrench: Refactor suggestion_

**중복되는 타입 정의 통합 필요**

`ReviewsResponse`와 `ReviewData` 타입이 동일한 구조를 가지고 있습니다.

다음과 같이 타입을 통합하는 것이 좋겠습니다:

```diff
-// 실제 데이터 응답값
-export type ReviewsResponse = {
-  data: Review[];
-  totalItemCount: number;
-  currentPage: number;
-  totalPages: number;
-};
-
-// 리뷰 정제 데이터
-export type ReviewData = {
-  reviews: Review[];
-  totalItemCount: number;
-  currentPage: number;
-  totalPages: number;
-};
+export type ReviewsResponse = {
+  data: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};
+
+export type ReviewData = Omit<ReviewsResponse, 'data'> & {
+  reviews: Review[];
+};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
// 실제 데이터 응답값
export type ReviewsResponse = {
  data: Review[];
  totalItemCount: number;
  currentPage: number;
  totalPages: number;
};

export type ReviewData = Omit<ReviewsResponse, 'data'> & {
  reviews: Review[];
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +1,54 @@
+import { useMemo } from "react";
+import Arrow from "@/images/arrow.svg";
+import { cn } from "@/utils/classnames";
+
+type PaginationProps = {
+  currentPage: number;
+  totalPages: number;
+  onClick: (page: number) => void;
+  pageLimit?: number;
+};
+
+export default function Pagination({ currentPage, totalPages, onClick, pageLimit = 6 }: PaginationProps) {
+  const currentStartPage = useMemo(() => {
+    return Math.floor((currentPage - 1) / pageLimit) * pageLimit + 1;
+  }, [currentPage, pageLimit]);
+
+  // 현재 페이지 그룹에서 보여줄 페이지 목록 생성
+  const pages = useMemo(() => {
+    return Array.from(
+      { length: Math.min(pageLimit, totalPages - currentStartPage + 1) },
+      (_, i) => currentStartPage + i,
+    );
+  }, [currentStartPage, pageLimit, totalPages]);
+
+  return (
+    <div className="mt-6 flex items-center justify-center gap-2 text-gray-200">
+      <button
+        onClick={() => onClick(currentStartPage - pageLimit)}
+        disabled={currentPage <= pageLimit}
+        aria-label="이전 페이지 그룹으로 이동"
+      >
+        <Arrow className={cn(currentPage > pageLimit && "text-black")} />
+      </button>
+      {pages.map((page) => (
+        <button
+          key={page}
+          onClick={() => onClick(page)}
+          aria-label={`${page}페이지로 이동`}
+          aria-current={currentPage === page && "page"}
+          className={cn("size-12 text-gray-500", currentPage === page && "font-semibold text-black")}
+        >
+          {page}
+        </button>
+      ))}
+      <button
+        onClick={() => onClick(currentStartPage + pageLimit)}
+        disabled={currentStartPage + pageLimit > totalPages}
+        aria-label="다음 페이지 그룹으로 이동"
+      >
+        <Arrow className={cn("scale-x-[-1] transform", currentStartPage + pageLimit <= totalPages && "text-black")} />
+      </button>
+    </div>
+  );
+}

리뷰: _:hammer_and_wrench: Refactor suggestion_

**다음 페이지 그룹 버튼 접근성 개선 필요**

다음 페이지 그룹 버튼에도 적절한 aria 레이블이 필요합니다.

다음과 같이 개선해보세요:

```diff
       <button
         onClick={() => onClick(currentStartPage + pageLimit)}
         disabled={currentStartPage + pageLimit > totalPages}
+        aria-label="다음 페이지 그룹으로 이동"
       >
         <Arrow className={cn("scale-x-[-1] transform", currentStartPage + pageLimit <= totalPages && "text-black")} />
       </button>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      <button
        onClick={() => onClick(currentStartPage + pageLimit)}
        disabled={currentStartPage + pageLimit > totalPages}
        aria-label="다음 페이지 그룹으로 이동"
      >
        <Arrow className={cn("scale-x-[-1] transform", currentStartPage + pageLimit <= totalPages && "text-black")} />
      </button>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +1,54 @@
+import { useMemo } from "react";
+import Arrow from "@/images/arrow.svg";
+import { cn } from "@/utils/classnames";
+
+type PaginationProps = {
+  currentPage: number;
+  totalPages: number;
+  onClick: (page: number) => void;
+  pageLimit?: number;
+};
+
+export default function Pagination({ currentPage, totalPages, onClick, pageLimit = 6 }: PaginationProps) {
+  const currentStartPage = useMemo(() => {
+    return Math.floor((currentPage - 1) / pageLimit) * pageLimit + 1;
+  }, [currentPage, pageLimit]);
+
+  // 현재 페이지 그룹에서 보여줄 페이지 목록 생성
+  const pages = useMemo(() => {
+    return Array.from(
+      { length: Math.min(pageLimit, totalPages - currentStartPage + 1) },
+      (_, i) => currentStartPage + i,
+    );
+  }, [currentStartPage, pageLimit, totalPages]);
+
+  return (
+    <div className="mt-6 flex items-center justify-center gap-2 text-gray-200">
+      <button
+        onClick={() => onClick(currentStartPage - pageLimit)}
+        disabled={currentPage <= pageLimit}
+        aria-label="이전 페이지 그룹으로 이동"
+      >
+        <Arrow className={cn(currentPage > pageLimit && "text-black")} />
+      </button>
+      {pages.map((page) => (
+        <button
+          key={page}
+          onClick={() => onClick(page)}
+          aria-label={`${page}페이지로 이동`}
+          aria-current={currentPage === page && "page"}
+          className={cn("size-12 text-gray-500", currentPage === page && "font-semibold text-black")}
+        >
+          {page}
+        </button>
+      ))}
+      <button
+        onClick={() => onClick(currentStartPage + pageLimit)}
+        disabled={currentStartPage + pageLimit > totalPages}
+        aria-label="다음 페이지 그룹으로 이동"
+      >
+        <Arrow className={cn("scale-x-[-1] transform", currentStartPage + pageLimit <= totalPages && "text-black")} />
+      </button>
+    </div>
+  );
+}

리뷰: _:hammer_and_wrench: Refactor suggestion_

**접근성 개선 필요**

페이지네이션 버튼에 적절한 aria 레이블이 없습니다.

다음과 같이 접근성을 개선해보세요:

```diff
-      <button onClick={() => onClick(currentStartPage - pageLimit)} disabled={currentPage <= pageLimit}>
+      <button
+        onClick={() => onClick(currentStartPage - pageLimit)}
+        disabled={currentPage <= pageLimit}
+        aria-label="이전 페이지 그룹으로 이동"
+      >
         <Arrow className={cn(currentPage > pageLimit && "text-black")} />
       </button>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    <div className="mt-6 flex items-center justify-center gap-2 text-gray-200">
      <button
        onClick={() => onClick(currentStartPage - pageLimit)}
        disabled={currentPage <= pageLimit}
        aria-label="이전 페이지 그룹으로 이동"
      >
        <Arrow className={cn(currentPage > pageLimit && "text-black")} />
      </button>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -1,8 +1,8 @@
 export type User = {
   id: number;
-  email: string;
+  email?: string;
   name: string;
-  companyName: string;
+  companyName?: string;
   image: string | null;
 };
 
@@ -13,3 +13,52 @@ export type GatheringParticipant = {
   joinedAt: string;
   User: User;
 };
+
+export type ReviewQuery = {
+  gatheringId?: string;
+  useId?: number;
+  type?: string;
+  location?: string;
+  date?: string;
+  registrationEnd?: string;
+  sortBy?: "createdAt" | "score" | "participantCount";
+  sortOrder?: "asc" | "desc";
+  limit?: number;
+  offset?: number;
+};
+
+type Gathering = {
+  teamId: string;
+  id: number;
+  type: string;
+  name: string;
+  dateTime: string;
+  location: string;
+  image: string;
+};
+
+export type Review = {
+  teamId?: string;
+  id: number;
+  score: number;
+  comment: string;
+  createdAt: string;
+  Gathering?: Gathering;
+  User: User;
+};
+
+// 실제 데이터 응답값
+export type ReviewsResponse = {
+  data: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};
+
+// 리뷰 정제 데이터
+export type ReviewData = {
+  reviews: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};

리뷰: ReviewData는 보기에는 ReviewsResponse랑 같아 보이는데 무슨 용도의 타입인가요?
@@ -1,8 +1,8 @@
 export type User = {
   id: number;
-  email: string;
+  email?: string;
   name: string;
-  companyName: string;
+  companyName?: string;
   image: string | null;
 };
 
@@ -13,3 +13,52 @@ export type GatheringParticipant = {
   joinedAt: string;
   User: User;
 };
+
+export type ReviewQuery = {
+  gatheringId?: string;
+  useId?: number;
+  type?: string;
+  location?: string;
+  date?: string;
+  registrationEnd?: string;
+  sortBy?: "createdAt" | "score" | "participantCount";
+  sortOrder?: "asc" | "desc";
+  limit?: number;
+  offset?: number;
+};
+
+type Gathering = {
+  teamId: string;
+  id: number;
+  type: string;
+  name: string;
+  dateTime: string;
+  location: string;
+  image: string;
+};
+
+export type Review = {
+  teamId?: string;
+  id: number;
+  score: number;
+  comment: string;
+  createdAt: string;
+  Gathering?: Gathering;
+  User: User;
+};
+
+// 실제 데이터 응답값
+export type ReviewsResponse = {
+  data: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};
+
+// 리뷰 정제 데이터
+export type ReviewData = {
+  reviews: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};

리뷰: ReviewsResponse는 실제 응답 데이터 타입이고 ReviewData는 임시로 해논 건데 지금 코드 리팩토링 마치면 타입 정리할 것 같아융..
@@ -1,8 +1,8 @@
 export type User = {
   id: number;
-  email: string;
+  email?: string;
   name: string;
-  companyName: string;
+  companyName?: string;
   image: string | null;
 };
 
@@ -13,3 +13,52 @@ export type GatheringParticipant = {
   joinedAt: string;
   User: User;
 };
+
+export type ReviewQuery = {
+  gatheringId?: string;
+  useId?: number;
+  type?: string;
+  location?: string;
+  date?: string;
+  registrationEnd?: string;
+  sortBy?: "createdAt" | "score" | "participantCount";
+  sortOrder?: "asc" | "desc";
+  limit?: number;
+  offset?: number;
+};
+
+type Gathering = {
+  teamId: string;
+  id: number;
+  type: string;
+  name: string;
+  dateTime: string;
+  location: string;
+  image: string;
+};
+
+export type Review = {
+  teamId?: string;
+  id: number;
+  score: number;
+  comment: string;
+  createdAt: string;
+  Gathering?: Gathering;
+  User: User;
+};
+
+// 실제 데이터 응답값
+export type ReviewsResponse = {
+  data: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};
+
+// 리뷰 정제 데이터
+export type ReviewData = {
+  reviews: Review[];
+  totalItemCount: number;
+  currentPage: number;
+  totalPages: number;
+};

리뷰: 넵 알겠습니다.
@@ -0,0 +1,7 @@
+package nextstep.security.role;
+
+import java.util.Collection;
+
+public interface RoleHierarchy {
+    Collection<GrantedAuthority> getReachableRoles(Collection<GrantedAuthority> authorities);
+}

리뷰: > 그런데 Spring Security 코드 전반에서는 Authority를 주로 사용하는데,
> RoleHierarchy에서 특별히 Role이라는 용어를 사용하는 이유가 궁금하네요.
> 이렇게 Role이라는 용어를 사용하는 이유는 역할로 비교한다고 명시적으로 보여주기 위해서 일까요??

혹시 어디 코드에서 `RoleHierarchy.getReachableRoles`로 되어있을까요? 실제 스프링 시큐리티 코드에서도 말씀해주신대로 authority라는 표현을 사용하고 있어요.
```java
	Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
			Collection<? extends GrantedAuthority> authorities);
```
https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchy.java
@@ -0,0 +1,10 @@
+package nextstep.security.role;
+
+import java.util.Collection;
+
+public class NullRoleHierarchy implements RoleHierarchy {
+    @Override
+    public Collection<GrantedAuthority> getReachableRoles(Collection<GrantedAuthority> authorities) {
+        return authorities;
+    }
+}

리뷰: `RoleHierarchy`의 영문명 그대로 이 인터페이스의 역할은 "역할 계층"을 만들어주는 것이에요. 그래서 구현체들도 역할 계층을 만들어주는 것으로 구성이 되어야하는데요.

`NullRoleHierarchy`이라는 것은 null이라는 말이 표현되어있기는하지만 직역하면 "역할 계층이 없다"입니다. 계층이 없다는 것이지 authority가 없다는 뜻은 아니기 때문에 empty를 반환하기보다는 인입된 파라메터를 그대로 반환해주는 것이 그 목적이라고 볼 수 있어요.
@@ -0,0 +1,7 @@
+package nextstep.security.role;
+
+import java.util.Collection;
+
+public interface RoleHierarchy {
+    Collection<GrantedAuthority> getReachableRoles(Collection<GrantedAuthority> authorities);
+}

리뷰: 제가 잘못 질문을 드렸네요. 죄송합니다.
getReachableRoles는 미션 코드에 부분이였네요. 😅

그래도 Role 이름은 시큐리티의 RoleHierarchy의 구현체 코드에도 종종 사용되는 것 같아요
예를 들어서 getReachableGrantedAuthorities 의 구현 메서드 중
``` java
			Set<GrantedAuthority> lowerRoles = this.rolesReachableInOneOrMoreStepsMap.get(authority.getAuthority());
			if (lowerRoles == null) {
				continue; // No hierarchy for the role
			}
			for (GrantedAuthority role : lowerRoles) {
				if (processedNames.add(role.getAuthority())) {
					reachableRoles.add(role);
				}
			}
```

위 부분들 보면 Authority 와 Role을 같이 사용하는 듯해서 RolePrefix와 관련있는건가? 했습니다 ㅎㅎ

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java
@@ -0,0 +1,10 @@
+package nextstep.security.role;
+
+import java.util.Collection;
+
+public class NullRoleHierarchy implements RoleHierarchy {
+    @Override
+    public Collection<GrantedAuthority> getReachableRoles(Collection<GrantedAuthority> authorities) {
+        return authorities;
+    }
+}

리뷰: 아하 저도 NULL을 왜 쓰지 라는 의문이 있었는데 , 빈 권한을 주는 것이 아니라 말 그대로 RoleHierarchy 가 없다라는 의미군요!