Java는 꾸준히 발전하면서 개발자가 더 안전하고 간결한 코드를 작성할 수 있도록 새로운 기능을 도입해 왔습니다
대표적으로 Java 5에서 추가된 제네릭(Generic), Java 8에서 도입된 람다(Lambda) 입니다
이러한 제네릭과 람다를 Spring에서 어떻게 활용되고 왜 사용하는지 알아보려고 합니다
1. 제네릭 (Generic)
제네릭(Generic)은 클래스나 메서드가 사용할 타입을 파라미터처럼 일반화하여 다룰 수 있도록 해주는 문법입니다
제네릭은 Java 5에서 처음 도입된 기능입니다 그 이전에는 List, Map과 같은 자료구조 컬렉션을 사용할 때 타입 안정성이 보장되지 않았습니다 List에 문자열과 정수를 동시에 넣을 수 있었으며 값을 꺼낼 때는 무조건 Object 타입으로 반환되기 때문에 직접 캐스팅해야 하는 불편함이 존재했습니다 이러한 과정에서 런타임 오류도 발생하기도 했습니다
이런 문제점을 해결하기 위해서 제네릭은 컴파일 시점에서 타입을 명확히 지정하여 타입 안정성 및 불필요한 캐스팅을 줄여줍니다
- 타입 안정성 보장: 잘못된 타입의 객체가 들어가는 것을 방지
- 형변환 제거: 불필요한 Type 캐스팅 코드가 사라짐
- 재사용성 강화: 하나의 클래스/메서드로 다양한 타입을 지원
JpaRepository와 제네릭

Spring Data JPA는 제네릭 기반 인터페이스 상속 구조입니다
@NoRepositoryBean
public interface JpaRepository<T, ID>
extends ListCrudRepository<T, ID>,
ListPagingAndSortingRepository<T, ID>,
QueryByExampleExecutor<T> {
}
JpaRepository<T, ID>는 단순히 CRUD만 제공하는 것이 아닌 여러 상위 인터페이스를 조합하여 여러 가지 기능을 제공합니다
- T: 실제 사용할 엔티티(Entity) 타입
- ID: 그 엔티티의 PK(Primary Key) 타입
1. ListCrudRepository<T, ID>
- 기본 CRUD 기능을 제공 (save,findById,findAll,deleteById 등)
- 반환 타입을 List로 통일 -> 별도 캐스팅이나 변환이 필요 X
2. ListPagingAndSortingRepository<T, ID>
- 페이징과 정렬 기능 (findAll(Pageable pageable), findAll(Sort sort))
- 결과를 List로 반환
3. QueryByExampleExecutor<T>
- Example 객체를 기반으로 한 동적 쿼리 지원
- findAll(Example<T>), count(Example<T>) 같은 메서드 사용 가능
public interface ExampleJpaRepository extends JpaRepository<T,ID> {
// save,findById,deleteById 등 기본 쿼리 메서드를 그대로 사용 가능
// 원하는 쿼리 메서드를 정의하여 사용
}
JpaRepository를 상속받아 엔티티 전용 Repository 인터페이스를 정의하여 사용하게 됩니다
여기서 JpaRepository<T,ID>를 보면 제네릭을 사용된 걸 볼 수 있습니다
위에서 설명한 내용과 동일하게 T는 사용할 엔티티의 타입 ID는 엔티티의 PK의 타입을 정확히 지정하여 사용해야 합니다
제네릭을 적용하여 JpaRepository는 안전하고, 중복이 최소화되며 재사용이 가능한 DB 접근 코드를 작성할 수 있습니다
public interface UserRepository extends JpaRepository<User, Long> { }
UserRepository는 JpaRepository<User,Long>을 상속받아 User 엔티티를 데이터베이스에서 다루기 위한 Repository 인터페이스입니다 직접 SQL 작성이나 구현체를 작성하지 않아도 Spring Data JPA가 런타임에 자동으로 구현체를 자동으로 생성
User 테이블에 접근하는 전용 DAO라고 할 수 있습니다
2. 람다(Lambda)
람다는 기존의 익명 클래스를 간결하게 표현할 수 있게 해주는 문법입니다
하나의 추상 메서드만 가진 함수형 인터페이스의 구현체를 간결하게 표현
람다는 Java 8에서 도입된 기능입니다 Java는 전통적으로 객체지향(OOP) 중심 언어라 함수형 프로그래밍을 지원하지 않았습니다
대표적으로 Python,JavaScript 등에서 익명 함수(anonymous function) 즉 람다 표현식이 사용되면서 생산성이 크게 증가하였으며 Java도 병렬 처리,스트림 API 등에 활용하기 위해 람다를 도입하게 되었습니다
- 간결함: 불필요한 익명 클래스 문법 제거
- 함수형 프로그래밍 지원: Stream API, map/filter 등 고차 함수 활용 가능
- 가독성 향상: 데이터 변환/필터링 로직을 짧게 표현
- 병렬 처리 지원: Stream API의 parallelStream()과 함께 사용하면 효과적
// 기본 형태
(매개변수) -> { 실행문 }
람다식은 기본적으로 (매개변수) -> {실행문} 형태를 가지고 있습니다
왼쪽은 매개변수 부분으로 메서드처럼 인자를 입력받을 수 있으며 오른쪽은 실행문 부분으로 { } 안에 실행할 코드를 작성합니다
마지막으로 -> (화살표 연산자)는 매개변수와 실행문을 구분하는 역할을 합니다.
1) 매개변수가 없는 경우
() -> System.out.println("Hello");
입력값이 없이 실행문만 실행되는 함수
2) 매개변수가 하나인 경우
name -> System.out.println(name);
매개변수가 하나인 경우 ( ) 소괄호 생략 가능
3) 매개변수가 여러 개인 경우
(x,y) -> { return x + y; }
두 값을 받아 합계를 반환하는 함수로 실행문이 한 줄이면 { } 생략 가능
간단하게 사용할 때는 위와 같이 작성하여 사용할 수 있으며 람다식으로 코드를 짧고 직관적으로 관리할 수 있는 장점이 있습니다
다음으로 Java의 자료구조(Collection)와 Spring의 Service 계층에서 어떻게 활용할 수 있는지 알아보겠습니다
람다식 자료구조 Collection과 Service 계층 활용
Java의 Collection은 데이터를 효율적으로 저장·검색·관리하기 위한 자료구조 모음입니다
간단하게 주요 Collection을 나열하면 다음과 같습니다
- List: 순서가 있는 데이터 집합 (중복 허용)
- Set: 순서가 없고 중복을 허용하지 않음
- Map: key-value 쌍으로 데이터 관리
1. List로 데이터 가공
@Service
public class StudentService {
public List<String> getStudentNames() {
List<String> names = Arrays.asList("kim", "lee", "park");
// 람다 + 스트림으로 가공
return names.stream()
.map(String::toUpperCase)
.toList();
}
}
학생 이름 목록을 불러와 대문자로 변환하는 메서드를 Service 계층에 구현할 때 람다식을 사용하면 간결하게 작성할 수 있습니다
names.stream() 으로 List<String> -> Stream<String> 으로 변환하여 컬렉션 요소를 하나씩 가공할 수 있는 데이터 파이프라인을 만들어 .map(String::toUpperCase)를 통해 각 문자열을 대문자로 변환합니다 .map은 각 요소를 변환하는 단계를 의미합니다 마지막으로 .toList()를 통해 변환된 스트림 요소를 다시 List로 수집하여 최종 결과는 List<String> 타입으로 반환합니다
하지만 람다식을 사용하지 않고 for문을 사용하여 구현한다면 다음과 같습니다
@Service
public class StudentService {
public List<String> getStudentNames() {
List<String> names = Arrays.asList("kim", "lee", "park");
List<String> upperNames = new ArrayList<>();
for (String name : names) {
upperNames.add(name.toUpperCase());
}
return upperNames;
}
}
for문을 사용하면 직관적이면 Java의 람다식을 모르는 초보자도 이해하기 쉽지만 코드가 길어지는 단점이 존재합니다
반면 람다/스트림을 사용하면 간결하고 데이터 흐름이 한눈에 보이는 장점이 있습니다
다음으로 Spring에서 주로 사용되는 예제에 대해 다뤄보도록 하겠습니다
2. 댓글 조회 -> DTO 변환
저희는 해당 게시글에 작성된 모든 댓글을 DB에서 가져와 DTO로 변환하여 반환하는 메서드를 구현하려고 합니다
여기서 람다식을 사용하여 코드를 작성하면 다음 코드로 간결하게 작성할 수 있습니다
public class CommentResponse {
private Long id;
private String content;
private String author;
// Comment 엔티티를 받아서 DTO로 변환하는 생성자
public CommentResponse(Comment comment) {
this.id = comment.getId();
this.content = comment.getContent();
this.author = comment.getAuthor().getName();
}
// getter 생략
}
comment 엔티티를 받아 DTO 객체를 만드는 생성자를 사전에 정의해 놓은 상태입니다
return commentRepository.findAllByPost(post).stream()
.map(comment -> new CommentResponse(comment))
.toList();
commentRepository.findAllByPost(post) -> 특정 게시글에 달린 모든 댓글 조회 (List<Comment>)
.stream( ) -> 스트림으로 변환하여 각 요소를 함수형 스타일로 조작할 수 있도록 만들어 줌
.map(comment -> new CommentResponse(comment)) -> 각 Comment 객체를 CommentResponse DTO로 변환
.toList() -> 최종적으로 List<CommentResponse> 반환
기존 for문 버전의 코드
List<Comment> comments = commentRepository.findAllByPost(post);
List<CommentResponse> responses = new ArrayList<>();
for (Comment comment : comments) {
responses.add(new CommentResponse(comment));
}
return responses;
람다식을 사용하지 않고 for문으로 구현한 코드입니다
3. 복잡한 비즈니스 로직에 람다식 사용
복잡한 비즈니스 로직에서 사용되는 람다식에 대해 설명하려고 합니다
1. 삭제가 안된 댓글이어야 한다
2. 좋아요는 10개 이상
3. 좋아요 수 기준으로 내림차순 정렬
다음과 같은 요구사항이 있다고 가정하고 람다식을 적용한 코드를 작성하면 다음과 같습니다
아래 코드에 사용된 Entity의 구조와 연관관계는 임의로 가정하에 진행된 코드입니다
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
// 특정 게시글의 인기 댓글 조회 (좋아요 10개 이상, 삭제되지 않은 댓글만)
@Transactional(readOnly = true)
public List<CommentResponse> getPopularComments(Post post) {
return commentRepository.findAllByPost(post).stream()
.filter(comment -> !comment.isDeleted()) // 삭제 안 된 댓글
.filter(comment -> comment.getLikeCount() > 10) // 좋아요 10개 이상
.sorted(Comparator.comparing(Comment::getLikeCount)
.reversed()) // 좋아요 수 기준 내림차순
.map(CommentResponse::new) // DTO 변환
.toList();
}
}
1. commentRepository.findAllByPost(post)
- 특정 게시글에 달린 모든 댓글을 조회(List<Comment>)
2. .Stream( )
- 스트림으로 변환하여 파이프라인 형태로 가공
3. .filter(comment -> !.comment.isDeleted())
- 삭제되지 않은 댓글만 남김
4. .filter(comment -> comment.getLikeCount() > 10)
- 좋아요가 10개 초과인 댓글만 필터링
5. .sorted(Comparator.comparing(Comment::getLikeCount).reversed())
- 좋아요 수 기준 내림차순 정렬
6. .map(CommentResponse::new)
- Comment 엔티티를 CommentResponse DTO로 변환
7. .toList()
- 최종적으로 List<CommentResponse> 반환
이처럼 람다와 스트림은 조건이 많아지고 데이터 가공이 복잡해질수록 더 큰 효과를 가져오며 Service 계층에서 여러 조건을 거쳐 DTO를 반환하는 비즈니스 로직은 람다식의 장점을 가장 잘 보여주는 활용 사례라고 볼 수 있습니다
제네릭과 람다는 Spring의 Service 계층에서 중복을 줄이고 가독성을 높이며 유지보수성을 강화하는 핵심 도구입니다
이렇게 람다식을 Service 계층에 사용하게 되면 많은 장점이 있기 때문에 많이 활용하는 게 좋을 거 같습니다
'Back-end > Spring' 카테고리의 다른 글
| [Spring] Spring Boot + MySQL 연동 & JPA 활용 심화 (1) (0) | 2025.12.30 |
|---|---|
| [Spring] Spring Data JPA 어노테이션 정리 (초기화 전략,연관관계,Repository) (0) | 2025.11.06 |
| [Spring] DTO와 ResponseEntity로 깔끔한 응답 처리하기 (1) | 2025.08.28 |
| [Spring] Spring Boot Controller & Service & Repository 구조 이해하기 (MVC 패턴) (2) | 2025.08.27 |
| [Spring] Spring Boot로 간단한 API 만들기 (Hello REST API) (1) | 2025.08.26 |