Java

함수형 파라미터를 두번 전달하는 이슈 정리

ybs 2022. 1. 5. 23:46
반응형

PR 리뷰를 하다 함수형 파라미터를 두 번 전달하는 코드를 만났다. 처음보는 패턴이라 코드가 어렵게 느껴졌다. 그래서 핵심부분을 재구성해서 정리했다.

 

재구성한 코드를 먼저 살펴보자. channel 안에는 여러개 room 이 존재하고 다시 room 안에는 여러명의 user 가 존재한다. 그래서 총 user size 는 room 갯수 X user 갯수가 된다.

@Value
@Builder
public class ChannelRequest {

	@Valid
	List<RoomRequest> rooms;

	@Value
	@Builder
	public static class RoomRequest {

		@Valid
		List<UserRequest> users;
	}

	@Value
	@Builder
	public static class UserRequest {}

	public int getTotalUserSize() {
		return this.rooms.stream()
			.mapToInt(it -> it.getUsers().size())
			.sum();
	}
}

 

하고싶은 작업은 아래와 같다.

 

1. RoomRequest 객체 정보를 아래 Room 객체로, UserRequest 객체 정보를 아래 User 객체로 전달.

2. 그런데 아래 User 객체의 id 값은 전달받지 않고 자체 정책으로 generate 시켜서 셋팅.

 

그 중 두번째 작업에 포커싱해서 설명하기 위해  Room 과 User 객체 나머지 필드들은 모두 생략했다.

@Value
@Builder
public class Channel {

	@NotEmpty
	List<Room> rooms;

	@Value
	@Builder
	public static class Room {

		@NotEmpty
		@Valid
		List<User> users;
	}

	@Value
	@Builder
	public static class User {
		@NotBlank
		String id;
	}
}

 

먼저 id 값을 어떻게 generate 시킬건지 살펴보자. 아래 CreateChannelService.create 메서드에서는 CreateChannels.create 메서드를 호출하는데, 전달받은 ChannelRequest 객체와 generateUserIds 메서드 레퍼런스 즉 함수를 넘긴다.

 

generateUserIds 메서드 구현을 보면 날짜 Instant 와 size(몇개를 만들건지)를 전달받는다. 그래서 날짜를 prefix로 하고 size 만큼 만들었다(generate 구현코드는 실제와 다르다).

public class CreateChannelService {

	public Channel create(ChannelRequest request) {
		return CreateChannels.create(
			request,
			this::generateUserIds
			// 람다 (prefixInstant, size) -> generateUserIds(prefixInstant, size)
		);
	}

	private List<String> generateUserIds(Instant prefixInstant, int size) {
		String prefix = prefixInstant.atZone(ZoneId.of("Asia/Seoul"))
			.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
			.substring(0, 8);

		List<Long> idList = this.generate(size);

		return idList.stream()
			.map(it -> prefix.concat("-" + it.toString()))
			.collect(toUnmodifiableList());
	}

	// 실제 generate 코드는 아님. 한번 사용되고 끝내는 toy 용으로 만듦.
	private List<Long> generate(int size) {
		return LongStream
			.rangeClosed(1, size)
			.boxed()
			.collect(toList());
	}
}

 

위에서 generateUserIds 메서드 레퍼런스를 넘겼기 때문에 아래 CreateChannels.create 메서드는 BiFunction<Instant, Integer, List<String>> 타입으로 받았다.

 

그리고 BiFunction<Instant, Integer, List<String>> 타입인 generateUserIds 를 getUserIdIterator 메서드로 다시 전달한다. 이때 Instant.now 로 현재 시간을 구한 값을 넣어서 전달한다. 여기서 다시 함수로 넘기는게  이 글의 핵심이다.

 

이전 함수를 그대로 넘기는게 아니라 현재시간 값을 추가해서 넘겼기 때문에 타입은 Function<Integer, List<String>> 로 바꼈다. 그리고 getUserIdIterator 에서 size 만큼 generate 해서 담긴 list 를 Iterator 로 변환 후 리턴해 최종적으로 userIds 변수가 받는다.

public class CreateChannels {
	public static Channel create(
		ChannelRequest request,
		BiFunction<Instant, Integer, List<String>> generateUserIds
	) {
		Instant userIdPrefixDateTime = Instant.now();
		Iterator<String> userIds = getUserIdIterator(
			request,
			// 핵심
			size -> generateUserIds.apply(userIdPrefixDateTime, size)
		);
	
		...
	}

	private static Iterator<String> getUserIdIterator(
		ChannelRequest channelRequest,
		Function<Integer, List<String>> generateUserIds
	) {
		int size = channelRequest.getTotalUserSize();
		return generateUserIds.apply(size).iterator();
	}
}

 

userIds 는 generate 된 값들을 갖고 있고 이제 User 객체 id 필드에 넣어줄 차례다. 아래 코드를 보면 Channel 의 rooms 값을 채우기 위해 ChannelRequest.getRooms().stream() 을 사용하고, map 연산자 안 toRoom 메서드로 userIds 를 전달한다. 그리고 다시 toUsers 메서드로 userIds 를 전달하고, toUsers 메서드에서 user 갯수 만큼 userIds.next 를 호출해서 id 값을 채운다.

public class CreateChannels {
	public static Channel create(
		ChannelRequest request,
		BiFunction<Instant, Integer, List<String>> generateUserIds
	) {
		Instant userIdPrefixDateTime = Instant.now();
		Iterator<String> userIds = getUserIdList(
			request,
			size -> generateUserIds.apply(userIdPrefixDateTime, size)
		);

		return Channel.builder()
			.rooms(
				request.getRooms().stream()
					.map(it -> toRoom(userIds, it))
					.collect(toUnmodifiableList())
			)
			.build();
	}

	private static Room toRoom(
		Iterator<String> userIds,
		RoomRequest roomRequest
	) {
		return Room.builder()
			.users(toUsers(userIds, roomRequest))
			.build();
	}

	private static List<User> toUsers(
		Iterator<String> userIds,
		RoomRequest roomRequest
	) {
		List<User> users = new ArrayList<>();
		List<UserRequest> userRequests = roomRequest.getUsers();
		for (UserRequest _i : userRequests) {
			users.add(toUser(userIds.next()));
		}
		return users;
	}

	private static User toUser(String userId) {
		return User.builder()
			.id(userId)
			.build();
	}
}

 

위 코드는 id 필드만 존재하기 때문에 extract method 가 과하고, 함수형 파라미터를 두번이나 전달하는게 불필요해 보일 수 있다. 하지만 실제 코드는 수십개의 필드가 각각 다양한 정책에 의해 변환이 이뤄지고 셋팅된다. 그래서 가독성을 위해 extract method 를 많이 하게 됐고 그 과정에서 lazy evaluate 한 작업이 안쪽에 있다보니 위와 같은 구조가 됐다.

반응형