[Spring] 19. @PathVariable이란?

[Spring] 19. @PathVariable이란?

이전 글에서는 @RequestParam에 대해 살펴봤습니다. @RequestParam은 URL의 쿼리 문자열이나 폼 데이터로 전달된 값을 Controller 메서드에서 받을 때 사용하는 어노테이션입니다.

이번 글에서는 URL 경로 자체에 포함된 값을 받는 @PathVariable에 대해 정리하겠습니다. 게시글 번호, 회원 번호, 상품 번호처럼 특정 자원을 식별하는 값은 URL 경로에 포함해서 표현할 수 있습니다.

핵심은 간단합니다. @PathVariable은 URL 경로의 일부 값을 Controller 메서드의 파라미터로 받을 때 사용하는 어노테이션입니다.


1. @PathVariable이란?

@PathVariable은 URL 경로 안에 들어있는 값을 꺼내서 Controller 메서드에서 사용할 수 있게 해줍니다.

예를 들어 게시글 번호가 10번인 상세 페이지를 요청한다고 생각해보겠습니다.

/board/10

위 URL에서 10은 단순한 문자열이 아니라 게시글 번호를 의미합니다. 이 값을 Controller에서 받기 위해 @PathVariable을 사용할 수 있습니다.

@GetMapping("/board/{boardNo}")
public String detail(@PathVariable Long boardNo) {
    System.out.println("게시글 번호: " + boardNo);
    return "board/detail";
}

위 코드에서 {boardNo}는 URL 경로에서 값을 받을 자리입니다. 사용자가 /board/10으로 요청하면 10boardNo에 들어갑니다.

GET /board/10
 → {boardNo} 위치의 값 10 추출
 → Long boardNo에 10 전달

2. @RequestParam과 @PathVariable 차이

@RequestParam@PathVariable은 둘 다 요청 값을 받는 방법입니다. 하지만 값이 들어있는 위치가 다릅니다.

구분 @RequestParam @PathVariable
값 위치 URL 뒤의 쿼리 문자열 URL 경로의 일부
예시 URL /board/detail?boardNo=10 /board/10
사용 예시 @RequestParam Long boardNo @PathVariable Long boardNo
주 사용 목적 검색어, 페이지 번호, 정렬 조건 게시글 번호, 회원 번호, 상품 번호

쉽게 구분하면 다음과 같습니다.

@RequestParam
 → 조건값을 받을 때 자주 사용
 → 검색어, 페이지, 카테고리

@PathVariable
 → 특정 자원을 식별할 때 자주 사용
 → 게시글 번호, 회원 번호, 상품 번호

3. 기본 사용법

@PathVariable을 사용하려면 URL 경로에 중괄호를 사용합니다.

@GetMapping("/board/{boardNo}")
public String detail(@PathVariable Long boardNo) {
    return "board/detail";
}

여기서 {boardNo}와 메서드 파라미터 boardNo의 이름이 같습니다. Spring은 URL 경로에서 해당 값을 꺼내 boardNo에 넣어줍니다.

요청 URL 추출 값 Controller 파라미터
/board/10 10 Long boardNo
/board/25 25 Long boardNo

URL은 달라지지만 같은 Controller 메서드가 실행됩니다. 다른 점은 경로에 들어있는 값만 달라지는 것입니다.


4. 이름을 직접 지정하기

URL 경로 변수 이름과 메서드 파라미터 이름이 다르면 @PathVariable에 이름을 직접 지정할 수 있습니다.

@GetMapping("/board/{boardNo}")
public String detail(@PathVariable("boardNo") Long no) {
    System.out.println("게시글 번호: " + no);
    return "board/detail";
}

위 코드에서는 URL 경로 변수 이름은 boardNo이고, 메서드 파라미터 이름은 no입니다. 이름이 다르기 때문에 @PathVariable("boardNo")로 어떤 값을 받을지 지정했습니다.

/board/10
 → {boardNo} = 10
 → Long no = 10

처음 공부할 때는 경로 변수 이름과 파라미터 이름을 같게 맞추는 것이 가장 읽기 좋습니다.


5. 게시글 상세 조회 예시

게시글 상세 조회 기능을 @PathVariable로 작성해보겠습니다.

@Controller
@RequestMapping("/board")
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }

    @GetMapping("/{boardNo}")
    public String detail(@PathVariable Long boardNo, Model model) {
        BoardDto board = boardService.findBoard(boardNo);
        model.addAttribute("board", board);
        return "board/detail";
    }
}

클래스 위에 @RequestMapping("/board")가 있으므로 메서드의 @GetMapping("/{boardNo}")와 합쳐져 최종 URL은 다음과 같습니다.

/board/{boardNo}

예를 들어 사용자가 /board/10으로 요청하면 boardNo 값은 10이 됩니다.

GET /board/10
 → boardNo = 10
 → boardService.findBoard(10)
 → board/detail 화면 반환

6. 여러 개의 PathVariable 받기

URL 경로 안에서 여러 개의 값을 받을 수도 있습니다.

/members/5/orders/20

위 URL에서는 회원 번호와 주문 번호를 함께 표현하고 있습니다.

@GetMapping("/members/{memberNo}/orders/{orderNo}")
public String orderDetail(
        @PathVariable Long memberNo,
        @PathVariable Long orderNo
) {
    System.out.println("회원 번호: " + memberNo);
    System.out.println("주문 번호: " + orderNo);
    return "order/detail";
}

요청 값은 다음처럼 연결됩니다.

URL 경로 경로 변수
/members/5 memberNo 5
/orders/20 orderNo 20

이처럼 URL 경로를 계층적으로 구성하면 어떤 자원에 접근하는지 더 명확하게 표현할 수 있습니다.


7. REST 스타일 URL

@PathVariable은 REST 스타일 URL에서 자주 사용됩니다. REST 스타일 URL은 자원을 경로로 표현하는 방식입니다.

기능 URL 예시 의미
게시글 목록 /boards 게시글 전체 목록
게시글 상세 /boards/10 10번 게시글
회원 상세 /members/5 5번 회원
상품 상세 /products/3 3번 상품

이런 구조에서는 URL만 봐도 어떤 자원을 요청하는지 알기 쉽습니다.

/boards/10
 → boards 중에서 10번 게시글

/members/5
 → members 중에서 5번 회원

그래서 REST API를 만들 때 @PathVariable이 자주 사용됩니다.


8. @RequestParam과 함께 사용하기

@PathVariable@RequestParam은 함께 사용할 수도 있습니다.

/boards/10/comments?page=2

위 URL에서 10은 게시글 번호이고, page=2는 댓글 페이지 번호입니다.

@GetMapping("/boards/{boardNo}/comments")
public String comments(
        @PathVariable Long boardNo,
        @RequestParam(defaultValue = "1") int page
) {
    System.out.println("게시글 번호: " + boardNo);
    System.out.println("댓글 페이지: " + page);
    return "comment/list";
}

이렇게 역할을 나누면 URL 구조가 더 명확해집니다.

boardNo 어떤 게시글의 댓글인지 식별하는 값
page 댓글 목록의 몇 페이지를 볼지 정하는 조건값

즉, 자원을 식별하는 값은 @PathVariable, 조회 조건이나 옵션은 @RequestParam으로 받는 식으로 구분할 수 있습니다.


9. 수정과 삭제 URL 예시

게시글 수정과 삭제 기능에서도 @PathVariable을 사용할 수 있습니다.

@Controller
@RequestMapping("/board")
public class BoardController {

    @GetMapping("/{boardNo}/edit")
    public String editForm(@PathVariable Long boardNo, Model model) {
        return "board/edit";
    }

    @PostMapping("/{boardNo}/edit")
    public String edit(@PathVariable Long boardNo, BoardDto boardDto) {
        return "redirect:/board/" + boardNo;
    }

    @PostMapping("/{boardNo}/delete")
    public String delete(@PathVariable Long boardNo) {
        return "redirect:/board/list";
    }
}

URL 구조를 보면 어떤 게시글을 수정하거나 삭제하는지 알 수 있습니다.

요청 역할 boardNo
GET /board/10/edit 10번 게시글 수정 화면 10
POST /board/10/edit 10번 게시글 수정 처리 10
POST /board/10/delete 10번 게시글 삭제 처리 10

이런 방식은 URL이 조금 더 의미 있게 보인다는 장점이 있습니다.


10. 타입 변환

URL 경로에 들어있는 값은 처음에는 문자열입니다. 하지만 Controller 메서드 파라미터 타입을 Long, int 등으로 지정하면 Spring이 가능한 경우 자동으로 타입을 변환해줍니다.

/board/10
@GetMapping("/board/{boardNo}")
public String detail(@PathVariable Long boardNo) {
    return "board/detail";
}

위 요청에서 URL의 10은 문자열로 들어오지만, Spring이 Long 타입으로 변환해줄 수 있습니다.

"10"
 → Long 타입 10

하지만 숫자로 변환할 수 없는 값이 들어오면 오류가 발생할 수 있습니다.

/board/abc
 → abc는 Long으로 변환할 수 없음
 → 타입 변환 오류 발생 가능

그래서 번호를 받는 URL에는 실제 숫자 값이 들어오도록 링크나 요청을 구성해야 합니다.


11. @PathVariable을 쓰기 좋은 상황

@PathVariable은 모든 요청 값에 사용하는 것이 아니라, 특정 자원을 식별하는 값에 사용하면 좋습니다.

상황 예시
게시글 하나를 조회 /boards/10
회원 하나를 조회 /members/5
상품 하나를 조회 /products/3
특정 게시글의 댓글 목록 /boards/10/comments
특정 회원의 주문 목록 /members/5/orders

반면 검색어, 페이지 번호, 정렬 조건처럼 조회 조건에 가까운 값은 @RequestParam이 더 자연스러운 경우가 많습니다.

자원 식별
 → @PathVariable

조회 조건
 → @RequestParam

12. 자주 하는 실수

1) 중괄호 이름과 파라미터 이름이 다른 경우

경로 변수 이름과 파라미터 이름이 다르면 값을 받지 못할 수 있습니다.

@GetMapping("/board/{boardNo}")
public String detail(@PathVariable Long no) {
    return "board/detail";
}

위 코드에서는 URL 경로 변수 이름은 boardNo인데 파라미터 이름은 no입니다. 이런 경우에는 이름을 직접 지정하는 것이 안전합니다.

@GetMapping("/board/{boardNo}")
public String detail(@PathVariable("boardNo") Long no) {
    return "board/detail";
}

2) 숫자 타입에 문자가 들어오는 경우

Long boardNo로 받는 URL에 숫자가 아닌 문자가 들어오면 타입 변환 오류가 발생할 수 있습니다.

/board/abc
 → Long boardNo로 변환 불가

번호로 받는 값은 숫자 링크가 만들어지도록 주의해야 합니다.

3) 검색 조건까지 PathVariable로 만들려는 경우

검색어, 페이지 번호, 정렬 조건은 보통 @RequestParam이 더 자연스럽습니다.

권장
/board/list?keyword=spring&page=2

비권장 예시
/board/list/spring/2

검색 조건이 많아질수록 URL 경로에 전부 넣으면 구조가 복잡해집니다. 이런 값은 쿼리 문자열로 보내는 편이 관리하기 쉽습니다.

4) URL 경로와 View 이름을 헷갈리는 경우

@GetMapping("/board/{boardNo}")
public String detail(@PathVariable Long boardNo) {
    return "board/detail";
}

여기서 /board/{boardNo}는 요청 URL 패턴입니다. board/detail은 View 이름입니다. 둘은 역할이 다릅니다.

5) 삭제 요청을 GET으로 처리하는 경우

삭제처럼 데이터를 변경하는 요청은 GET보다 POST 또는 DELETE를 사용하는 것이 좋습니다. JSP 기반 게시판에서는 삭제 버튼을 form으로 만들고 POST로 처리하는 경우가 많습니다.

@PostMapping("/board/{boardNo}/delete")
public String delete(@PathVariable Long boardNo) {
    return "redirect:/board/list";
}

주소창 접근이나 링크 클릭만으로 삭제가 실행되는 구조는 피하는 것이 좋습니다.


13. 게시판 Controller 전체 예시

게시판 기능을 @PathVariable과 함께 구성하면 다음처럼 작성할 수 있습니다.

@Controller
@RequestMapping("/board")
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }

    @GetMapping("/list")
    public String list(
            @RequestParam(value = "keyword", required = false) String keyword,
            @RequestParam(value = "page", defaultValue = "1") int page,
            Model model
    ) {
        List<BoardDto> boardList = boardService.findBoardList(keyword, page);
        model.addAttribute("boardList", boardList);
        return "board/list";
    }

    @GetMapping("/{boardNo}")
    public String detail(@PathVariable Long boardNo, Model model) {
        BoardDto board = boardService.findBoard(boardNo);
        model.addAttribute("board", board);
        return "board/detail";
    }

    @GetMapping("/{boardNo}/edit")
    public String editForm(@PathVariable Long boardNo, Model model) {
        BoardDto board = boardService.findBoard(boardNo);
        model.addAttribute("board", board);
        return "board/edit";
    }

    @PostMapping("/{boardNo}/edit")
    public String edit(@PathVariable Long boardNo, BoardDto boardDto) {
        boardService.updateBoard(boardNo, boardDto);
        return "redirect:/board/" + boardNo;
    }

    @PostMapping("/{boardNo}/delete")
    public String delete(@PathVariable Long boardNo) {
        boardService.deleteBoard(boardNo);
        return "redirect:/board/list";
    }
}

이 예시에서 게시글 번호는 URL 경로에 포함했습니다. 반면 검색어와 페이지 번호는 조회 조건이므로 @RequestParam으로 받았습니다.

게시글 번호
 → 특정 게시글을 식별
 → @PathVariable

검색어, 페이지 번호
 → 목록 조회 조건
 → @RequestParam

정리

@PathVariable은 URL 경로의 일부 값을 Controller 메서드에서 받을 때 사용하는 어노테이션입니다. /board/10처럼 특정 자원을 식별하는 값을 URL에 포함할 때 자주 사용합니다.

@PathVariable URL 경로 일부 값을 메서드 파라미터로 받습니다.
{boardNo} URL 경로에서 값을 받을 자리입니다.
@RequestParam 쿼리 문자열이나 폼 파라미터 값을 받을 때 사용합니다.

처음에는 다음 기준으로 구분하면 좋습니다.

특정 자원을 식별한다
 → @PathVariable

검색, 페이지, 정렬 조건이다
 → @RequestParam

입력값이 많다
 → DTO

이 기준을 잡아두면 URL 설계와 Controller 파라미터 처리가 훨씬 명확해집니다.


연습해보기

아래 URL을 Controller에서 받아보세요.

GET /board/10
GET /members/5
GET /products/3
GET /boards/10/comments?page=2

예시는 다음과 같습니다.

@GetMapping("/boards/{boardNo}/comments")
public String comments(
        @PathVariable Long boardNo,
        @RequestParam(defaultValue = "1") int page
) {
    return "comment/list";
}

다음 글에서는 요청 데이터를 객체로 받는 방법인 @ModelAttribute와 DTO 바인딩에 대해 정리하겠습니다. 폼 입력값이 여러 개일 때 Controller에서 어떻게 깔끔하게 받는지 살펴보겠습니다.