Claude Chat vs Cowork vs Code 비교

|

Anthropic의 Claude는 단일 제품이 아니라, 같은 모델(Opus, Sonnet, Haiku)을 사용하지만 둘러싼 도구와 자율성의 수준이 다른 세 가지 제품군으로 구성되어 있습니다.


  1. Claude Chat (생각하는 도구)

가장 익숙한 형태입니다. claude.ai 웹앱이나 데스크톱/모바일 앱에서 대화하는 인터페이스로, 질문하고 답을 받는 전통적인 챗봇 방식입니다.

  • 사고, 브레인스토밍, 글쓰기, 분석, 리서치를 위한 인터페이스
  • 업무 중 떠오르는 빠른 질문에 적합
  • Claude가 일에 대해 생각하지만, 실제로 파일을 만지거나 시스템을 조작하지는 않음

적합한 사용처: 아이디어 정리, 글 초안 작성, 개념 설명, 코드 리뷰 토론, 일반적인 질의응답


  1. Claude Cowork (위임하는 도구)

2026년 1월 Anthropic Labs의 일부로 출시되었고, 4월부터 macOS와 Windows에서 정식 출시된 자율 에이전트입니다. Claude 데스크톱 앱 내부에서 실행됩니다.

Chat과의 결정적인 차이는 Chat이 일에 대해 생각한다면, Cowork은 일을 수행한다는 점입니다. 비개발자도 터미널 없이 Claude Code의 파일시스템 능력을 활용할 수 있도록 만든 제품입니다.

가능한 작업 예시:

  • 어수선한 다운로드 폴더 정리
  • 스프레드시트 분석 및 결과 요약
  • 원시 데이터로부터 보고서 생성
  • 여러 문서로부터 리서치 자료 컴파일

주요 기능:

  • 컴퓨터의 폴더에 직접 접근
  • Claude Code의 계획 및 에이전트 기능 활용
  • Claude for Chrome과 페어링하여 기존 커넥터 사용 가능
  • 각 단계에 대한 사용자 승인 절차

적합한 사용처: 파일 정리, 데이터 분석, 보고서 작성, 여러 앱을 넘나드는 지식 업무 자동화


  1. Claude Code (개발하는 도구)

터미널(CLI) 기반의 에이전트 코딩 도구입니다. 개발자가 자신의 코드 저장소에서 Claude에게 작업을 위임할 수 있습니다.

  • 파일을 직접 읽고, 수정하고, 명령을 실행
  • 자율적으로 빌드 및 디버깅
  • CLAUDE.md 파일로 프로젝트 맥락을 영구적으로 유지
  • MCP 서버 통합으로 외부 시스템 연결

적합한 사용처: 신규 기능 개발, 리팩터링, 버그 수정, 테스트 작성, 빌드 파이프라인 자동화


한눈에 비교

구분 Chat Cowork Code
용도 생각·대화 지식 업무 위임 코드 작업·개발
인터페이스 웹/앱 채팅창 데스크톱 앱 모드 터미널(CLI)
대상 누구나 비개발자 지식근로자 개발자
자율성 낮음 중간(승인 단계) 높음
접근 범위 대화 로컬 파일 + 커넥터 + 브라우저 코드베이스 + 시스템 전반
메모리 채팅 간 메모리, 프로젝트 프로젝트, 예약 작업, 커넥터 설정 CLAUDE.md + 자동 메모리

쉽게 고르는 법

  • 답이나 아이디어가 필요하면Chat
  • 결과물(파일, 정리된 데이터, 보고서)이 필요하면Cowork
  • 코드를 작성·수정·빌드해야 하면Code

비유로 이해하기

한 명의 직원이 책상(Chat), 전문 장비가 있는 작업장(Cowork), 또는 시스템 전체 접근권이 있는 기계실(Code)에서 일할 수 있다고 생각하면 됩니다.

같은 두뇌, 다른 장비.

각 모드의 지능은 동일합니다. 달라지는 것은 그 지능을 둘러싼 도구이며, 그 도구가 무엇을 할 수 있는지를 결정합니다.

(Kotlin) tailrec을 이용한 꼬리 재귀 최적화

|

꼬리 재귀 (Tail Recursion)

재귀 호출이 함수의 마지막 연산인 경우를 꼬리 재귀라 한다. 일반 재귀는 호출마다 스택 프레임이 쌓여 깊은 재귀 시 StackOverflowError가 발생하지만, 꼬리 재귀는 이전 스택 프레임을 재사용할 수 있어 루프로 변환이 가능하다.

tailrec 키워드

Kotlin에서 tailrec 키워드를 함수에 붙이면 컴파일러가 꼬리 재귀를 루프로 최적화해준다.

tailrec fun factorial(n: Int, acc: Long = 1L): Long =
    if (n <= 1) acc
    else factorial(n - 1, n * acc)

위 코드는 컴파일 시 내부적으로 아래와 같은 루프로 변환된다.

fun factorial(n: Int, acc: Long = 1L): Long {
    var n = n; var acc = acc
    while (n > 1) { acc *= n; n-- }
    return acc
}

꼬리 재귀가 아닌 경우

재귀 호출 이후 추가 연산이 있으면 꼬리 재귀가 아니므로 tailrec 최적화가 적용되지 않는다.

// 꼬리 재귀 X - 재귀 호출 후 곱셈 연산이 남아있음
tailrec fun factorial(n: Int): Long =
    if (n <= 1) 1L
    else n * factorial(n - 1) // 경고 발생: 꼬리 재귀가 아님

이 경우 컴파일러가 경고를 출력하며 최적화가 적용되지 않는다.

사용 조건

  • 함수의 마지막 연산이 자기 자신 호출이어야 한다.
  • open이나 override 함수에는 사용할 수 없다.
  • 조건을 만족하지 않으면 컴파일러가 경고를 준다 (최적화 안 됨).

활용 예시

리스트 합계

tailrec fun sum(list: List<Int>, acc: Int = 0): Int =
    if (list.isEmpty()) acc
    else sum(list.drop(1), acc + list.first())

피보나치

tailrec fun fibonacci(n: Int, a: Long = 0L, b: Long = 1L): Long =
    when (n) {
        0 -> a
        1 -> b
        else -> fibonacci(n - 1, b, a + b)
    }

문자열 뒤집기

tailrec fun reverse(s: String, acc: String = ""): String =
    if (s.isEmpty()) acc
    else reverse(s.drop(1), s.first() + acc)

정리

tailrec은 재귀 로직의 가독성을 유지하면서 루프 수준의 성능을 얻을 수 있는 기능이다. 누적값을 파라미터로 전달하는 패턴(accumulator pattern)을 사용하면 대부분의 재귀를 꼬리 재귀로 변환할 수 있다.

(Spring Kafka) JacksonJsonDeserializer 설정 옵션 정리

|

JacksonJsonDeserializer

Spring Kafka가 제공하는 JSON 역직렬화기로, Kafka 메시지의 value를 Jackson으로 Java/Kotlin 객체로 변환한다.

val javaType = jacksonObjectMapper().constructType(typeRef)
val deserializer = JacksonJsonDeserializer<T>(javaType).apply {
    addTrustedPackages("com.foo")
    setUseTypeHeaders(false)
}

addTrustedPackages

역직렬화 시 허용할 패키지를 지정하는 보안 설정이다.

  • Spring Kafka의 JsonDeserializer는 기본적으로 임의의 클래스 역직렬화를 차단한다 (원격 코드 실행 등 보안 위협 방지).
  • 역직렬화 대상 클래스가 신뢰된 패키지에 속하지 않으면 예외가 발생한다.
  • addTrustedPackages("*")로 모든 패키지를 허용할 수도 있지만, 프로덕션에서는 필요한 패키지만 명시하는 것이 권장된다.
// com.foo 하위 클래스만 역직렬화 허용
addTrustedPackages("com.foo")

setUseTypeHeaders

Kafka 메시지 헤더에 포함된 타입 정보를 사용할지 여부를 설정한다.

  • true (기본값): 메시지 헤더(__TypeId__)에 담긴 클래스 정보를 읽어 해당 타입으로 역직렬화한다. Producer가 JsonSerializer로 보낸 메시지에는 이 헤더가 자동으로 붙는다.
  • false: 헤더를 무시하고, 생성자에서 지정한 타입으로만 역직렬화한다.
// 헤더 무시 → 항상 javaType으로 역직렬화
setUseTypeHeaders(false)

false로 설정하는 주요 사유는 다음과 같다.

  • Producer가 다른 시스템이라 __TypeId__ 헤더가 없거나 다른 클래스명을 사용하는 경우
  • 헤더의 타입 정보를 신뢰하지 않고 명시적으로 타입을 고정하고 싶은 경우

setRemoveTypeHeaders

역직렬화 후 타입 관련 헤더(__TypeId__, __KeyTypeId__, __ContentTypeId__)를 메시지에서 제거할지 여부를 설정한다.

  • true (기본값): 역직렬화 후 타입 헤더를 제거한다.
  • false: 타입 헤더를 그대로 유지한다. 하나의 메시지를 여러 Consumer가 순차적으로 읽거나, 다운스트림에서도 타입 정보가 필요한 경우에 사용한다.
setRemoveTypeHeaders(false)

setTypeMapper

기본 타입 매퍼(DefaultJackson2JavaTypeMapper)를 커스텀 매퍼로 교체한다. 타입 헤더의 해석 방식을 완전히 제어하고 싶을 때 사용한다.

val mapper = DefaultJackson2JavaTypeMapper().apply {
    setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence.TYPE_ID)
    addTrustedPackages("com.foo")
}
setTypeMapper(mapper)

setTypeFunction

바이트 배열과 헤더를 받아 동적으로 역직렬화 대상 JavaType을 결정하는 함수를 지정한다. 하나의 토픽에 여러 타입의 메시지가 섞여 있을 때 유용하다.

setTypeFunction { data, headers ->
    val typeHeader = String(headers.lastHeader("messageType").value())
    when (typeHeader) {
        "ORDER" -> jacksonObjectMapper().constructType(Order::class.java)
        "PAYMENT" -> jacksonObjectMapper().constructType(Payment::class.java)
        else -> jacksonObjectMapper().constructType(Map::class.java)
    }
}

setUseTypeMapperForKey

기본 타입 매퍼가 key 타입 헤더(__KeyTypeId__)를 참조하도록 설정한다. key를 JSON으로 역직렬화할 때 사용한다.

setUseTypeMapperForKey(true)

configure() 프로퍼티

setter 대신 Map<String, ?> 또는 Spring Boot 프로퍼티로 설정할 수도 있다.

프로퍼티 키 설명
spring.json.trusted.packages 신뢰 패키지 (쉼표 구분, *은 전체 허용)
spring.json.use.type.headers 타입 헤더 사용 여부
spring.json.remove.type.headers 역직렬화 후 타입 헤더 제거 여부
spring.json.value.default.type 타입 헤더 없을 때 기본 역직렬화 클래스
spring.json.key.default.type key의 기본 역직렬화 클래스
spring.json.type.mapping 토큰-클래스 매핑 (예: order:com.foo.Order,payment:com.foo.Payment)
spring.json.value.type.method value 타입 결정용 정적 메서드 (FQCN)
spring.json.key.type.method key 타입 결정용 정적 메서드 (FQCN)
# application.yml 예시
spring:
  kafka:
    consumer:
      properties:
        spring.json.trusted.packages: "com.foo"
        spring.json.use.type.headers: false
        spring.json.value.default.type: "com.foo.MyEvent"

주의: setter를 하나라도 호출한 뒤 configure()를 호출하면 프로퍼티 설정이 무시된다. setter 방식과 프로퍼티 방식을 혼용하지 않도록 한다.

Fluent API

메서드 체이닝을 지원하는 Fluent 스타일 메서드도 제공된다.

val deserializer = JacksonJsonDeserializer<MyEvent>(javaType)
    .trustedPackages("com.foo")
    .ignoreTypeHeaders()          // setUseTypeHeaders(false) 와 동일
    .dontRemoveTypeHeaders()      // setRemoveTypeHeaders(false) 와 동일
    .forKeys()                    // key 역직렬화용으로 지정
    .typeFunction { data, headers -> /* JavaType 반환 */ }

조합 예시

javaType을 생성자에서 직접 넘기고 헤더를 끄면, 항상 호출자가 지정한 타입으로 역직렬화된다.

val javaType = jacksonObjectMapper().constructType(typeRef)
val deserializer = JacksonJsonDeserializer<T>(javaType).apply {
    addTrustedPackages("com.foo")   // 이 패키지의 클래스만 역직렬화 허용
    setUseTypeHeaders(false)        // 헤더 타입 무시, 지정한 javaType으로 고정
}

(Kotlin) reified를 이용한 런타임 타입 참조

|

타입 소거 (Type Erasure)

JVM에서 제네릭 타입 정보는 컴파일 이후 런타임에 소거된다. 따라서 아래와 같이 제네릭 타입을 직접 참조하는 코드는 컴파일 에러가 발생한다.

fun <T> printType() {
    println(T::class) // 컴파일 에러 - T의 타입을 알 수 없음
}

reified 키워드

inline 함수와 함께 reified를 사용하면 런타임에도 타입 정보를 유지할 수 있다.

inline fun <reified T> printType() {
    println(T::class) // 정상 동작
}

printType<String>() // class kotlin.String

왜 inline이 필요한가?

inline 함수는 호출 지점에 함수 본문이 복사된다. 컴파일러가 호출 시점에 실제 타입을 알고 있으므로, 복사할 때 T를 구체적인 타입으로 치환할 수 있다.

// 컴파일 전
printType<String>()

// 인라인 후 (개념적)
println(String::class)

활용 예시

타입 체크 / 캐스팅

inline fun <reified T> List<*>.filterByType(): List<T> {
    return this.filter { it is T }.map { it as T }
}

val mixed = listOf(1, "hello", 2, "world")
val strings = mixed.filterByType<String>() // ["hello", "world"]

Jackson/Gson 등 역직렬화

inline fun <reified T> String.fromJson(): T {
    return objectMapper.readValue(this, T::class.java)
}

val user = jsonString.fromJson<User>()

Android에서 클래스 참조 전달 생략

// reified 없이
startActivity(Intent(this, MainActivity::class.java))

// reified 사용
inline fun <reified T : Activity> Context.startActivity() {
    startActivity(Intent(this, T::class.java))
}
startActivity<MainActivity>()

제약사항

  • 반드시 inline 함수에서만 사용 가능
  • reified 타입 파라미터로 새 인스턴스 생성 불가 (T() 불가)
  • Java에서 호출 불가 (인라인 메커니즘이 Kotlin 전용)

(Kotlin) null을 컨트롤 하는 연산자

|

?. - 안전한 호출 (Safe calls)

  • null이 아닌 경우 실행한다.
  • let과 연계 사용시 더 간결하게 처리 가능
    // JAVA
    public String safetyUpperCase(String str) {
        if (str != null) {
            return str.toUpperCase();
        }
    }
    // kotlin
    fun safetyUpperCase(str:String?) = str?.uppercase()
    // str이 null일 경우 let 이하 구문은 실행되지 않는다
    fun combinePrefixIfNotNull(str:String?, prefix:String) = str?.let { prefix + str }

    val s1:String? = null
    val s2:String? = "A"
    combinePrefixIfNotNull(s1, "PREFIX-") // null
    combinePrefixIfNotNull(s2, "PREFIX-") // PREFIX-A

?: - 엘비스연산자 (Elvis operator)

  • null 대신 사용 할 디폴트 값을 지정
    // JAVA
    String value = str == null ? "DEFAULT" : str;
    // kotlin
    val value = str?: "DEFAULT"

as? - 안전한 캐스트 (Safe casts)

  • 지정한 타입으로 캐스트 하고 불가 시 null 반환
    // JAVA
    public Long toLong(Object obj) {
        if (obj instanceof Long) {
            return (Long)obj
        } else {
            return null;
        }
    }
    // kotlin
    fun toLong(obj:Any):Long? = obj as? Long

!! - 널 아님 단언 (not-null assertion)

  • null이 될 수 있는 타입을 null이 될 수 없는 타입으로 강제전환
  • 실제 값이 null일 경우 NPE 발생
  • 안티패턴 : person.company!!.address!!.country ** 어느 값에서 NPE가 발생했는지 알기 어렵다
    fun <T> notNullAssert(nullable:T?):T = nullable!!

    val s1:String? = "NOT-NULL"
    val s2:String? = null
    notNullAssert(s1)
    notNullAssert(s2)  // NullPointerException